πŸͺ„ Programming

[TS] νƒ€μž…μŠ€ν¬λ¦½νŠΈ λΈŒλžœλ””λ“œ νƒ€μž…

ColorFilter 2024. 9. 26. 23:20
λ°˜μ‘ν˜•

Branded Type


νƒ€μž…μŠ€ν¬λ¦½νŠΈλ₯Ό μ‚¬μš©ν•˜λ‹€ 보면 μ„œλ‘œ λ‹€λ₯Έ 두 객체의 νŠΉμ • 속성이 λ™μΌν•œ νƒ€μž…μ„ κ°€μ§ˆ λ•Œκ°€ λ§Žλ‹€. 이둜 인해 νƒ€μž… μ‹œμŠ€ν…œμ—μ„œ 였λ₯˜κ°€ λ°œμƒν•˜μ§€ μ•Šλ”λΌλ„ 논리적 였λ₯˜λ‚˜ νƒ€μž… μ•ˆμ „μ„± 문제λ₯Ό μ•ΌκΈ°ν•  수 μžˆλ‹€.

type User = {
  id: string;
  name: string;
};

type Post = {
  id: string;
  ownerId: string;
  comments: Comments[];
};

type Comments = {
  id: string;
  timestamp: string;
  body: string;
  authorId: string;
};

 

μœ„ μ˜ˆμ‹œμ—μ„œ User.id, Post.id, Comments.idλŠ” λͺ¨λ‘ string νƒ€μž…μ΄λ‹€. 첫 번째 인자 postId, 두 번째 인자 authorIdλ₯Ό λ°›λŠ” ν•¨μˆ˜μ— 인자 μˆœμ„œλ₯Ό λ°˜λŒ€λ‘œ 전달해도, 두 νƒ€μž…μ΄ λ¬Έμžμ—΄λ‘œ λ™μΌν•˜κΈ° λ•Œλ¬Έμ— 였λ₯˜λ₯Ό κ°μ§€ν•˜μ§€ λͺ»ν•œλ‹€. νƒ€μž… μ‹œμŠ€ν…œμ—μ„  μ—λŸ¬κ°€ λ°œμƒν•˜μ§€ μ•Šμ•˜μ§€λ§Œ, λŸ°νƒ€μž„μ—μ„  μˆœμ„œκ°€ 바뀐 id κ°’ λ•Œλ¬Έμ— 잘λͺ»λœ 응닡을 λ°›λŠ” λ¬Έμ œκ°€ λ°œμƒν•œλ‹€.

async function getCommentsForPost(postId: string, authorId: string) {
  const response = await api.get(
    `/author/${authorId}/posts/${postId}/comments`,
  );
  return response.data;
}

const comments = await getCommentsForPost(user.id, post.id); // νƒ€μž…μ—λŸ¬ λ°œμƒ μ•ˆν•¨

 

이런 문제λ₯Ό λ°©μ§€ν•˜κΈ° μœ„ν•΄ λΈŒλžœλ””λ“œ νƒ€μž…(Branded Types)을 μ‚¬μš©ν•  수 μžˆλ‹€. λΈŒλžœλ””λ“œ νƒ€μž…μ€ κΈ°μ‘΄ νƒ€μž…μ— κ³ μœ ν•œ μ‹λ³„μžλ₯Ό λ‚˜νƒ€λ‚΄λŠ” ν”„λ‘œνΌν‹°(라벨)λ₯Ό μΆ”κ°€ν•˜μ—¬ 보닀 λͺ…ν™•ν•˜κ³  ꡬ체적인 νƒ€μž…μ„ μƒμ„±ν•œλ‹€.

type Brand<K, T> = K & { __brand: T };
type UserID = Brand<string, "UserId">;
type PostID = Brand<string, "PostId">;

const userId = "123" as UserID;
console.log(typeof userId); // string
console.log(userId.__brand); // undefined

 

μœ„ μ½”λ“œμ—μ„œ Brand<K, T>λŠ” Kλ₯Ό κΈ°λ³Έ νƒ€μž…μœΌλ‘œ, Tλ₯Ό κ³ μœ ν•œ μ‹λ³„μžλ‘œ μ‚¬μš©ν•˜μ—¬ νƒ€μž…μ„ ν™•μž₯ν•˜κ³  μžˆλ‹€. 예λ₯Ό λ“€μ–΄ Kλ₯Ό string으둜 μ§€μ •ν•˜λ©΄, string νƒ€μž…μ— __brandλΌλŠ” 특수 ν”„λ‘œνΌν‹°κ°€ 뢙은 μƒˆλ‘œμš΄ νƒ€μž…μ΄ μƒμ„±λœλ‹€.

 

μ΄λ•Œ __brand ν”„λ‘œνΌν‹°λŠ” λŸ°νƒ€μž„μ—μ„  μ‘΄μž¬ν•˜μ§€ μ•Šκ³ , νƒ€μž…μŠ€ν¬λ¦½νŠΈ μ»΄νŒŒμΌλŸ¬κ°€ μ„œλ‘œ λ‹€λ₯Έ λΈŒλžœλ””λ“œ νƒ€μž…μ„ ꡬ뢄할 λ•Œ μ‚¬μš©ν•˜λŠ” 메타데이터닀. 이λ₯Ό 톡해 λ™μΌν•œ μ›μ‹œ νƒ€μž…μΈ string을 μ‚¬μš©ν•˜λ”λΌλ„, μ»΄νŒŒμΌλŸ¬λŠ” μ„œλ‘œ λ‹€λ₯Έ νƒ€μž…μœΌλ‘œ μΈμ‹ν•˜μ—¬ νƒ€μž… μ•ˆμ „μ„±μ΄ κ°•ν™”λœλ‹€.

type Brand<K, T> = K & { __brand: T };

type UserID = Brand<string, "UserId">;
type PostID = Brand<string, "PostId">;
type CommentID = Brand<string, "CommentId">;

type User = {
  id: UserID;
  name: string;
};

type Post = {
  id: PostID;
  ownerId: string;
  comments: Comments[];
};

type Comments = {
  id: CommentID;
  timestamp: string;
  body: string;
  authorId: UserID;
};

async function getCommentsForPost(postId: PostID, authorId: UserID) {
  // ...
}

const user = {} as User;
const post = {} as Post;

// ❌ TS2345: Argument of type UserID is not assignable to parameter of type PostID
const comments = getCommentsForPost(user.id, post.id);

 

πŸ’‘ μ œλ„€λ¦­μ΄ μ›μ‹œ νƒ€μž…μΌ λ•Œλ„ ꡐ차 νƒ€μž…μ„ μ‚¬μš©ν•˜μ—¬(& μ—°μ‚°μž) νƒ€μž… λ ˆλ²¨μ—μ„œ 좔가적인 속성을 λΆ€μ—¬ν•  수 μžˆλ‹€. νƒ€μž…μ„ ν• λ‹Ήν•  λ•Œ ꡐ차 νƒ€μž… νŠΉμ„±μ— 따라 λͺ¨λ“  νŠΉμ„±μ„ ν¬ν•¨ν•œ ꡬ체적인 νƒ€μž…μœΌλ‘œ κ²°μ •λ˜λŠ”λ°(두 νƒ€μž…μ„ λͺ¨λ‘ λ§Œμ‘±ν•˜λŠ” ν•˜λ‚˜μ˜ νƒ€μž… 생성), μ œλ„€λ¦­μ— μ›μ‹œ νƒ€μž…μ„ λ„˜κ²Όλ‹€λ©΄ μΆ”κ°€λœ 속성은 νƒ€μž… μ•ˆμ •μ„±μ„ μœ„ν•œ μš©λ„λ‘œλ§Œ μ‚¬μš©λ˜κ³ , λŸ°νƒ€μž„μ—μ„  μ›μ‹œ νƒ€μž…μœΌλ‘œ ν‰κ°€λœλ‹€.

 

 

κ°œμ„ λœ Branded Type


μœ„μ—μ„œ μ‚¬μš©ν•œ __brand ν”„λ‘œνΌν‹°λŠ” λŸ°νƒ€μž„μ—λŠ” μ‘΄μž¬ν•˜μ§€ μ•Šμ§€λ§Œ, μ½”λ“œ μž‘μ„± μ‹œ μžλ™ 완성에 ν‘œμ‹œλΌμ„œ μ ‘κ·Ό κ°€λŠ₯ν•΄ λ³΄μ΄λŠ” 단점이 μžˆλ‹€. μ•„λž˜μ²˜λŸΌ μœ λ‹ˆν¬ 심볼을 ν™œμš©ν•˜λ©΄ 쀑볡 ν‚€ μ‚¬μš©μ„ λ°©μ§€ν•  수 있고, declare ν‚€μ›Œλ“œμ™€ ν•¨κ»˜ μ‚¬μš©ν•˜μ—¬ νƒ€μž… μ‹œμŠ€ν…œμ—λ§Œ μ‘΄μž¬ν•˜λŠ” __brand 심볼 ν”„λ‘œνΌν‹°λ₯Ό μ •μ˜ν•΄μ„œ 접근을 차단할 수 μžˆλ‹€.

declare const __brand: unique symbol;
type Brand<B> = { [__brand]: B };
type Branded<T, B> = T & Brand<B>;

type UserID = Branded<string, "UserId">;

const userId = "123" as UserID;
console.log(typeof userId); // string
console.log(userId.__brand); // TS2339: Property __brand does not exist on type UserID

 

unique symbol은 νƒ€μž… μ‹œμŠ€ν…œμ—μ„œ κ³ μœ μ„±μ„ λ”μš± κ°•ν•˜κ²Œ 보μž₯ν•˜μ—¬ 심볼 κ°„μ˜ μΆ©λŒμ„ λ°©μ§€ν•˜κΈ° μœ„ν•œ κΈ°λŠ₯이닀. μžλ°”μŠ€ν¬λ¦½νŠΈμ˜ Symbol은 κ³ μœ ν•œ 값을 κ°€μ§€μ§€λ§Œ, unique symbol을 μ‚¬μš©ν•˜λ©΄ νƒ€μž… μˆ˜μ€€μ—μ„œμ˜ κ³ μœ μ„±κΉŒμ§€ μ—„κ²©ν•˜κ²Œ 관리할 수 μžˆλ‹€. unique symbol은 const μ„ μ–Έμ΄λ‚˜ 클래슀의 readonly static μ†μ„±μ—μ„œλ§Œ μ‚¬μš©ν•  수 μžˆλ‹€.

 

declare ν‚€μ›Œλ“œλŠ” μ‹€μ œ κ΅¬ν˜„ 없이 νƒ€μž… μ •λ³΄λ§Œ μ„ μ–Έν•  λ•Œ μ‚¬μš©ν•œλ‹€. 주둜 μ™ΈλΆ€ λΌμ΄λΈŒλŸ¬λ¦¬λ‚˜ κΈ°μ‘΄ λͺ¨λ“ˆμ˜ νƒ€μž…μ„ μ •μ˜ν•˜μ—¬ νƒ€μž…μŠ€ν¬λ¦½νŠΈ μ»΄νŒŒμΌλŸ¬μ—κ²Œ νƒ€μž… 정보λ₯Ό μ•Œλ €μ£ΌλŠ” 역할을 ν•œλ‹€. declare둜 μ„ μ–Έλœ λ‚΄μš©μ€ 컴파일 μ‹œ νƒ€μž… μ²΄ν¬μ—λ§Œ μ‚¬μš©ν•˜κ³ , μ΅œμ’…μ μœΌλ‘œ μƒμ„±λ˜λŠ” μžλ°”μŠ€ν¬λ¦½νŠΈ μ½”λ“œμ—λŠ” ν¬ν•¨λ˜μ§€ μ•ŠλŠ”λ‹€.

 

 

Use Cases


μ‚¬μš©μž μž…λ ₯ 데이터λ₯Ό λ°›μ•„ μœ νš¨μ„±μ„ κ²€μ‚¬ν•œ ν›„, λΈŒλžœλ””λ“œ νƒ€μž…μ„ λ°˜ν™˜ν•˜μ—¬ 일반 λ¬Έμžμ—΄κ³Ό κ΅¬λΆ„ν•˜λŠ” λ°©μ‹μœΌλ‘œ νƒ€μž… μ•ˆμ •μ„±μ„ 더 κ°•ν™”ν•  수 μžˆλ‹€.

type EmailAddress = Brand<string, "EmailAddress">;
function validEmail(email: string): EmailAddress {
  // 이메일 μœ νš¨μ„± 검사 둜직
  return email as EmailAddress;
}

 

λΈŒλžœλ””λ“œ νƒ€μž…μ„ μ‚¬μš©ν•˜λ©΄ 더 ν‘œν˜„λ ₯ μžˆλŠ” μ½”λ“œλ₯Ό μž‘μ„±ν•  수 μžˆλ‹€. μ•„λž˜μ²˜λŸΌ CarBrand, EngineTypeκ³Ό 같은 ꡬ체적인 νƒ€μž…μ„ λ„μž…ν•¨μœΌλ‘œμ¨, 각 값듀이 μ–΄λ–€ 의미λ₯Ό κ°€μ§€λŠ”μ§€ μ •ν™•ν•˜κ²Œ λͺ…μ‹œν•  수 μžˆλ‹€.

type CarBrand = Brand<string, "CarBrand">;
type EngineType = Brand<string, "EngineType">;
type CarModel = Brand<string, "CarModel">;
type CarColor = Brand<string, "CarColor">;

function createCar(
  carBrand: CarBrand,
  carModel: CarModel,
  engineType: EngineType,
  color: CarColor,
): Car {
  // ...
}

// λ¬Έμžμ—΄ λ¦¬ν„°λŸ΄μ€ CarBrand, CarModel, EngineType, CarColor νƒ€μž…μ΄ μ•„λ‹ˆλ―€λ‘œ μ—λŸ¬ λ°œμƒ
const car = createCar("Toyota", "Corolla", "Diesel", "Red"); // ts2345 Error

 

 

레퍼런슀


 


κΈ€ μˆ˜μ •μ‚¬ν•­μ€ λ…Έμ…˜ νŽ˜μ΄μ§€μ— κ°€μž₯ λΉ λ₯΄κ²Œ λ°˜μ˜λ©λ‹ˆλ‹€. 링크λ₯Ό μ°Έκ³ ν•΄ μ£Όμ„Έμš”
λ°˜μ‘ν˜•