Pink Bobblehead Bunny νƒ€μž…μŠ€ν¬λ¦½νŠΈ νƒ€μž… ν˜Έν™˜μ„±
 

νƒ€μž…μŠ€ν¬λ¦½νŠΈ νƒ€μž… ν˜Έν™˜μ„±

좜처 : https://toss.tech/article/typescript-type-compatibility

μ•ˆλ…•ν•˜μ„Έμš”! μ˜€λŠ˜μ€ νƒ€μž…μŠ€ν¬λ¦½νŠΈλ₯Ό μ‚¬μš©ν•˜λ©΄μ„œ 늘 λ§ˆμ£ΌμΉ˜μ§€λ§Œ, 막상 μ„€λͺ…ν•˜λ €λ©΄ ν—·κ°ˆλ¦¬λŠ” 'νƒ€μž… ν˜Έν™˜μ„±(Type Compatibility)'에 λŒ€ν•΄ 깊이 νŒŒκ³ λ“€μ–΄ 보렀고 ν•©λ‹ˆλ‹€. 마침 ν† μŠ€ 기술 λΈ”λ‘œκ·Έμ— 올라온 νƒ€μž…μŠ€ν¬λ¦½νŠΈμ˜ νƒ€μž… ν˜Έν™˜μ„±, μ œλŒ€λ‘œ μ•Œκ³  κ³„μ‹ κ°€μš”?λΌλŠ” 글을 읽고 큰 도움을 λ°›μ•„μ„œ, μ œκ°€ κ³΅λΆ€ν•˜κ³  μ΄ν•΄ν•œ λ‚΄μš©μ„ λ°”νƒ•μœΌλ‘œ λΈ”λ‘œκ·Έμ— 정리해 보기둜 ν–ˆμŠ΅λ‹ˆλ‹€.


πŸ€” νƒ€μž… ν˜Έν™˜μ„±μ΄λž€? μ™œ μ€‘μš”ν• κΉŒ?

νƒ€μž…μŠ€ν¬λ¦½νŠΈμ—μ„œ νƒ€μž… ν˜Έν™˜μ„±μ΄λž€,  'νŠΉμ • νƒ€μž…μ˜ 값이 λ‹€λ₯Έ νƒ€μž…μ˜ κ°’μœΌλ‘œ 취급될 수 μžˆλŠ”κ°€?'λ₯Ό νŒλ‹¨ν•˜λŠ” κ·œμΉ™μž…λ‹ˆλ‹€. κ°„λ‹¨νžˆ 말해 AλΌλŠ” νƒ€μž…μ˜ λ³€μˆ˜μ— BλΌλŠ” νƒ€μž…μ˜ 값을 ν• λ‹Ήν•  수 μžˆλŠ”μ§€ κ²°μ •ν•˜λŠ” 것이죠.

let name: string = "μ•¨λ¦¬μŠ€";
let age: number = 30;

// name = age; // Error! Type 'number' is not assignable to type 'string'.

μœ„ 예제처럼 string νƒ€μž…μ— numberλ₯Ό ν• λ‹Ήν•  수 μ—†λŠ” 것은 λ„ˆλ¬΄λ‚˜ λ‹Ήμ—°ν•΄ λ³΄μž…λ‹ˆλ‹€. ν•˜μ§€λ§Œ νƒ€μž…μ΄ κ°μ²΄λ‚˜ ν•¨μˆ˜μ²˜λŸΌ λ³΅μž‘ν•΄μ§€κΈ° μ‹œμž‘ν•˜λ©΄, νƒ€μž…μŠ€ν¬λ¦½νŠΈκ°€ μ–΄λ–€ κΈ°μ€€μœΌλ‘œ ν˜Έν™˜μ„±μ„ νŒλ‹¨ν•˜λŠ”μ§€ λͺ…ν™•νžˆ μ•„λŠ” 것이 μ€‘μš”ν•΄μ§‘λ‹ˆλ‹€. 이 원리λ₯Ό μ œλŒ€λ‘œ 이해해야 예츑 λΆˆκ°€λŠ₯ν•œ νƒ€μž… μ—λŸ¬λ₯Ό 쀄이고, νƒ€μž…μŠ€ν¬λ¦½νŠΈμ˜ κ°•λ ₯ν•œ νƒ€μž… μΆ”λ‘  κΈ°λŠ₯을 100% ν™œμš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.


πŸ¦† 핡심 원칙: ꡬ쑰적 타이핑 (Structural Typing)

νƒ€μž…μŠ€ν¬λ¦½νŠΈ νƒ€μž… μ‹œμŠ€ν…œμ˜ κ°€μž₯ 큰 νŠΉμ§•μ€ ꡬ쑰적 타이핑을 λ”°λ₯Έλ‹€λŠ” μ μž…λ‹ˆλ‹€. 이름이 μ€‘μš”ν•œ 것이 μ•„λ‹ˆλΌ, κ°€μ§€κ³  μžˆλŠ” 속성(ꡬ쑰)이 μ€‘μš”ν•˜λ‹€λŠ” μ˜λ―Έμž…λ‹ˆλ‹€.

μ΄λŠ” ν”νžˆ '덕 타이핑(Duck Typing)'으둜 λΉ„μœ λ©λ‹ˆλ‹€. "였리처럼 κ±·κ³ , 였리처럼 κ½₯κ½₯κ±°λ¦°λ‹€λ©΄, 그것은 였리일 것이닀."λΌλŠ” λ§μ²˜λŸΌμš”.

interface Person {
  name: string;
}

interface Developer {
  name: string;
  skill: string;
}

let person: Person;
let developer: Developer = { name: "λ°₯", skill: "TypeScript" };

// κ°€λŠ₯! DeveloperλŠ” Person이 μš”κ΅¬ν•˜λŠ” 'name' 속성을 κ°€μ§€κ³  μžˆμœΌλ―€λ‘œ.
person = developer;

// λΆˆκ°€λŠ₯! Person은 Developerκ°€ μš”κ΅¬ν•˜λŠ” 'skill' 속성이 μ—†μœΌλ―€λ‘œ.
// developer = person; // Error! Property 'skill' is missing in type 'Person'...

μœ„ μ½”λ“œμ—μ„œ DeveloperλŠ” Person μΈν„°νŽ˜μ΄μŠ€λ₯Ό implements ν•˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. ν•˜μ§€λ§Œ DeveloperλŠ” Person이 ν•„μš”λ‘œ ν•˜λŠ” λͺ¨λ“  속성(name)을 κ°€μ§€κ³  있고, 심지어 skillμ΄λΌλŠ” 속성을 μΆ”κ°€λ‘œ 더 κ°€μ§€κ³  μžˆμŠ΅λ‹ˆλ‹€. λ”°λΌμ„œ νƒ€μž…μŠ€ν¬λ¦½νŠΈλŠ” Developerλ₯Ό Person νƒ€μž…μ— ν• λ‹Ήν•  수 μžˆλ‹€κ³  νŒλ‹¨ν•©λ‹ˆλ‹€. 더 λ§Žμ€ 속성을 κ°€μ§„ νƒ€μž…μ„ 더 적은 속성을 κ°€μ§„ νƒ€μž…μ— ν• λ‹Ήν•˜λŠ” 것은 μ•ˆμ „ν•˜κΈ° λ•Œλ¬Έμ΄μ£ .

✨ 잠깐! 그런데 이건 μ™œ μ—λŸ¬κ°€ λ‚ κΉŒ? - 초과 속성 검사

ꡬ쑰적 타이핑 μ›μΉ™λ§Œ μƒκ°ν•˜λ©΄ μ•„λž˜ μ½”λ“œλ„ λ¬Έμ œκ°€ μ—†μ–΄ λ³΄μž…λ‹ˆλ‹€.

interface Person {
  name: string;
}

function greet(person: Person) {
  console.log(`Hello, ${person.name}`);
}

// μ—λŸ¬ λ°œμƒ!
// Argument of type '{ name: string; age: number; }' is not assignable to parameter of type 'Person'.
// Object literal may only specify known properties, and 'age' does not exist in type 'Person'.
greet({ name: "찰리", age: 40 });

λΆ„λͺ… ageλΌλŠ” 초과 속성이 있긴 ν•˜μ§€λ§Œ, Person이 μš”κ΅¬ν•˜λŠ” name 속성을 λ§Œμ‘±ν•˜λŠ”λ° μ™œ μ—λŸ¬κ°€ λ‚ κΉŒμš”?

μ΄λŠ” νƒ€μž…μŠ€ν¬λ¦½νŠΈκ°€ 객체 λ¦¬ν„°λŸ΄ 에 λŒ€ν•΄ 초과 속성 검사(Excess Property Checks)λΌλŠ” νŠΉλ³„ν•œ 검사λ₯Ό μˆ˜ν–‰ν•˜κΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€. νƒ€μž…μŠ€ν¬λ¦½νŠΈλŠ” κ°œλ°œμžκ°€ μ•„λ§ˆλ„ age 속성을 μΆ”κ°€ν•˜λ €λ˜ 게 μ•„λ‹ˆλΌ, 속성 이름을 name λŒ€μ‹  naem처럼 μ˜€νƒ€λ₯Ό λƒˆμ„ κ°€λŠ₯성이 λ†’λ‹€κ³  νŒλ‹¨ν•˜κ³  이λ₯Ό μ—λŸ¬λ‘œ μ•Œλ €μ£ΌλŠ” κ²ƒμž…λ‹ˆλ‹€. μΌμ’…μ˜ μ•ˆμ „μž₯치인 μ…ˆμ΄μ£ .

이 검사λ₯Ό ν”Όν•˜κ³  μ‹Άλ‹€λ©΄, 객체 λ¦¬ν„°λŸ΄μ„ λ³€μˆ˜μ— λ¨Όμ € ν• λ‹Ήν•˜λ©΄ λ©λ‹ˆλ‹€.

const charlie = { name: "찰리", age: 40 };
greet(charlie); // OK!

charlie λ³€μˆ˜μ— ν• λ‹Ήλ˜λŠ” μˆœκ°„ 'μ‹ μ„ ν•œ(fresh)' 객체 λ¦¬ν„°λŸ΄μ΄ μ•„λ‹ˆκ²Œ λ˜λ―€λ‘œ, 초과 속성 검사λ₯Ό κ±΄λ„ˆλ›°κ³  ꡬ쑰적 타이핑 κ·œμΉ™λ§ŒμœΌλ‘œ νƒ€μž…μ„ κ²€μ‚¬ν•˜κ²Œ λ©λ‹ˆλ‹€.


🀯 κ°€μž₯ ν—·κ°ˆλ¦¬λŠ” λΆ€λΆ„: ν•¨μˆ˜μ˜ νƒ€μž… ν˜Έν™˜μ„±

ν•¨μˆ˜μ˜ νƒ€μž… ν˜Έν™˜μ„±μ€ 객체보닀 쑰금 더 λ³΅μž‘ν•©λ‹ˆλ‹€. λ§€κ°œλ³€μˆ˜μ™€ λ°˜ν™˜ νƒ€μž…, 두 κ°€μ§€λ₯Ό λͺ¨λ‘ κ³ λ €ν•΄μ•Ό ν•˜κΈ° λ•Œλ¬Έμ΄μ£ .

1. λ°˜ν™˜ νƒ€μž… (Return Type) - 곡변성 (Covariance)

λ°˜ν™˜ νƒ€μž…μ˜ ν˜Έν™˜μ„±μ€ 객체와 λΉ„μŠ·ν•΄μ„œ μ§κ΄€μ μž…λ‹ˆλ‹€. ν• λ‹Ήν•˜λ €λŠ” ν•¨μˆ˜μ˜ λ°˜ν™˜ νƒ€μž…μ΄ 할당될 ν•¨μˆ˜μ˜ λ°˜ν™˜ νƒ€μž…λ³΄λ‹€ 더 ꡬ체적인(μž‘μ€) νƒ€μž…μ΄μ–΄μ•Ό ν•©λ‹ˆλ‹€.

interface Person {
  name: string;
}

interface Developer extends Person {
  skill: string;
}

let personFactory: () => Person;
let developerFactory: () => Developer;

// κ°€λŠ₯! DeveloperλŠ” Person의 μ„œλΈŒνƒ€μž…μ΄λ―€λ‘œ.
personFactory = developerFactory;

// λΆˆκ°€λŠ₯! Person은 Developer의 λͺ¨λ“  속성을 λ§Œμ‘±ν•˜μ§€ λͺ»ν•˜λ―€λ‘œ.
// developerFactory = personFactory;

personFactoryλŠ” Person을 λ°˜ν™˜ν•˜λŠ” ν•¨μˆ˜λ₯Ό 담을 λ³€μˆ˜μž…λ‹ˆλ‹€. 여기에 Developerλ₯Ό λ°˜ν™˜ν•˜λŠ” developerFactoryλ₯Ό ν• λ‹Ήν•˜λŠ” 것은 μ•ˆμ „ν•©λ‹ˆλ‹€. DeveloperλŠ” Person의 λͺ¨λ“  속성을 ν¬ν•¨ν•˜κΈ° λ•Œλ¬Έμ—, personFactoryλ₯Ό ν˜ΈμΆœν•œ μͺ½μ—μ„œλŠ” κΈ°λŒ€ν–ˆλ˜ name 속성을 λ¬Έμ œμ—†μ΄ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

이λ₯Ό 곡변성(Covariance)이라고 ν•©λ‹ˆλ‹€. νƒ€μž…μ˜ 상속 관계 λ°©ν–₯κ³Ό ν• λ‹Ή κ°€λŠ₯μ„±μ˜ λ°©ν–₯이 κ°™λ‹€λŠ” μ˜λ―Έμž…λ‹ˆλ‹€.

2. λ§€κ°œλ³€μˆ˜ (Parameters) - λ°˜κ³΅λ³€μ„± (Contravariance)

λ§€κ°œλ³€μˆ˜λŠ” λ°˜ν™˜ νƒ€μž…κ³Ό μ •λ°˜λŒ€μž…λ‹ˆλ‹€. ν• λ‹Ήν•˜λ €λŠ” ν•¨μˆ˜μ˜ λ§€κ°œλ³€μˆ˜ νƒ€μž…μ΄ 할당될 ν•¨μˆ˜μ˜ λ§€κ°œλ³€μˆ˜ νƒ€μž…λ³΄λ‹€ 더 좔상적인(큰) νƒ€μž…μ΄μ–΄μ•Ό ν•©λ‹ˆλ‹€.

interface Person {
  name: string;
}

interface Developer extends Person {
  skill: string;
}

let greetPerson: (person: Person) => void;
let greetDeveloper: (dev: Developer) => void;

// λΆˆκ°€λŠ₯!
// greetPerson = greetDeveloper;

// κ°€λŠ₯!
greetDeveloper = greetPerson;

greetDeveloperλŠ” Developer νƒ€μž…μ„ 인자둜 λ°›λŠ” ν•¨μˆ˜μž…λ‹ˆλ‹€. 여기에 Person νƒ€μž…μ„ 인자둜 λ°›λŠ” greetPerson을 ν• λ‹Ήν•˜λŠ” 것이 μ™œ κ°€λŠ₯ν• κΉŒμš”?

greetDeveloper λ³€μˆ˜μ— greetPerson ν•¨μˆ˜λ₯Ό ν• λ‹Ήν–ˆλ‹€κ³  κ°€μ •ν•΄ λ΄…μ‹œλ‹€. 이제 greetDeveloperλ₯Ό ν˜ΈμΆœν•˜λ©΄ μ‹€μ œλ‘œλŠ” greetPerson이 μ‹€ν–‰λ©λ‹ˆλ‹€.

// greetDeveloper λ³€μˆ˜μ—λŠ” greetPerson ν•¨μˆ˜κ°€ λ‹΄κ²¨μžˆλ‹€.
greetDeveloper({ name: "데이브", skill: "React" });

greetDeveloperλ₯Ό ν˜ΈμΆœν•˜λŠ” μͺ½μ—μ„œλŠ” μ•½μ†λŒ€λ‘œ Developer 객체λ₯Ό 인자둜 λ„˜κΈΈ κ²ƒμž…λ‹ˆλ‹€. 이 κ°μ²΄λŠ” μ‹€μ œ 싀행될 greetPerson ν•¨μˆ˜λ‘œ μ „λ‹¬λ©λ‹ˆλ‹€. greetPerson ν•¨μˆ˜λŠ” Person νƒ€μž…μ˜ 인자λ₯Ό κΈ°λŒ€ν•˜λŠ”λ°, DeveloperλŠ” Person이 μš”κ΅¬ν•˜λŠ” name 속성을 κ°€μ§€κ³  μžˆμœΌλ―€λ‘œ 아무 λ¬Έμ œκ°€ μ—†μŠ΅λ‹ˆλ‹€.

λ°˜λŒ€μ˜ 경우λ₯Ό 생각해보면 μ™œ μ•ˆλ˜λŠ”μ§€ λͺ…ν™•ν•΄μ§‘λ‹ˆλ‹€. λ§Œμ•½ greetPerson λ³€μˆ˜μ— greetDeveloper ν•¨μˆ˜λ₯Ό ν• λ‹Ήν•œλ‹€λ©΄?

// greetPerson λ³€μˆ˜μ— greetDeveloper ν•¨μˆ˜κ°€ λ‹΄κ²¨μžˆλ‹€κ³  κ°€μ • (μ‹€μ œλ‘œλŠ” μ—λŸ¬)
greetPerson({ name: "이브" });

greetPerson을 ν˜ΈμΆœν•˜λŠ” μͺ½μ—μ„œλŠ” Person 객체λ₯Ό λ„˜κΈΈ κ²ƒμž…λ‹ˆλ‹€. 이 κ°μ²΄λŠ” μ‹€μ œ 싀행될 greetDeveloper ν•¨μˆ˜λ‘œ μ „λ‹¬λ©λ‹ˆλ‹€. ν•˜μ§€λ§Œ greetDeveloperλŠ” skill 속성을 κ°€μ§„ Developer 객체λ₯Ό κΈ°λŒ€ν–ˆλŠ”λ°, Person κ°μ²΄μ—λŠ” skill 속성이 μ—†μœΌλ―€λ‘œ λŸ°νƒ€μž„ μ—λŸ¬κ°€ λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€.

이처럼 λ§€κ°œλ³€μˆ˜μ˜ νƒ€μž… ν˜Έν™˜μ„±μ€ 상속 κ΄€κ³„μ˜ λ°©ν–₯κ³Ό λ°˜λŒ€μ΄κΈ° λ•Œλ¬Έμ— λ°˜κ³΅λ³€μ„±(Contravariance)이라고 ν•©λ‹ˆλ‹€.

πŸ’‘ 잠깐! 이변성(Bivariance)도 μžˆμ–΄μš” tsconfig.jsonμ—μ„œ "strictFunctionTypes": false μ˜΅μ…˜μ„ μ‚¬μš©ν•˜λ©΄ λ§€κ°œλ³€μˆ˜μ—μ„œ 곡변성과 λ°˜κ³΅λ³€μ„±μ„ λͺ¨λ‘ ν—ˆμš©ν•˜κ²Œ λ˜λŠ”λ°, 이λ₯Ό 이변성(Bivariance)이라고 ν•©λ‹ˆλ‹€. νƒ€μž… μ•ˆμ •μ„±μ„ μœ„ν•΄ true둜 μ„€μ •ν•˜λŠ” 것이 κ°•λ ₯ν•˜κ²Œ ꢌμž₯λ©λ‹ˆλ‹€.


⛓️ νƒ€μž… ν˜Έν™˜μ„± μ œμ–΄ν•˜κΈ°: Branded Type

λ•Œλ‘œλŠ” ꡬ쑰가 같더라도 μ˜λ„μ μœΌλ‘œ νƒ€μž…μ„ κ΅¬λΆ„ν•˜κ³  싢을 λ•Œκ°€ μžˆμŠ΅λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, UserId와 PostIdλŠ” λ‘˜ λ‹€ number νƒ€μž…μ΄μ§€λ§Œ, μ„œλ‘œ μ ˆλŒ€ μ„žμ—¬μ„œλŠ” μ•ˆ λ˜λŠ” κ°’μž…λ‹ˆλ‹€. μ΄λ•Œ Branded Type (λ˜λŠ” Nominal Type) 기법을 μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

type UserId = number & { readonly __brand: "UserId" };
type PostId = number & { readonly __brand: "PostId" };

function getUserId(id: number): UserId {
  return id as UserId;
}

function getPostId(id: number): PostId {
  return id as PostId;
}

function getUser(id: UserId) {
  // ...
}

const userId = getUserId(1);
const postId = getPostId(1);

getUser(userId); // OK
// getUser(postId); // Error! Type 'PostId' is not assignable to type 'UserId'.

μ‹€μ œλ‘œλŠ” μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” __brand 속성을 νƒ€μž…μ— μΆ”κ°€ν•¨μœΌλ‘œμ¨, 같은 number νƒ€μž…μ΄λΌλ„ ꡬ쑰적으둜 λ‹€λ₯΄κ²Œ λ§Œλ“€μ–΄ νƒ€μž…μŠ€ν¬λ¦½νŠΈκ°€ 두 νƒ€μž…μ„ κ΅¬λΆ„ν•˜λ„λ‘ λ§Œλ“œλŠ” νŠΈλ¦­μž…λ‹ˆλ‹€.


글을 마치며

νƒ€μž…μŠ€ν¬λ¦½νŠΈμ˜ νƒ€μž… ν˜Έν™˜μ„±, 특히 ν•¨μˆ˜μ˜ 곡변성과 λ°˜κ³΅λ³€μ„±μ€ μ²˜μŒμ—λŠ” λ¨Έλ¦Ώμ†μ—μ„œ 그림이 잘 κ·Έλ €μ§€μ§€ μ•ŠλŠ” μ–΄λ €μš΄ κ°œλ…μΌ 수 μžˆμŠ΅λ‹ˆλ‹€. ν•˜μ§€λ§Œ 이 원리λ₯Ό μ΄ν•΄ν•˜κ³  λ‚˜λ©΄ μ™œ νƒ€μž…μŠ€ν¬λ¦½νŠΈκ°€ νŠΉμ • μƒν™©μ—μ„œ μ—λŸ¬λ₯Ό λ°œμƒμ‹œν‚€λŠ”μ§€, 그리고 μ–΄λ–»κ²Œ νƒ€μž…μ„ 더 μ•ˆμ „ν•˜κ²Œ 섀계할 수 μžˆλŠ”μ§€μ— λŒ€ν•œ κΉŠμ€ 톡찰을 μ–»κ²Œ λ©λ‹ˆλ‹€.

μœ μ—°ν•¨κ³Ό μ•ˆμ •μ„± μ‚¬μ΄μ—μ„œ μ ˆλ¬˜ν•œ κ· ν˜•μ„ 작고 μžˆλŠ” νƒ€μž…μŠ€ν¬λ¦½νŠΈμ˜ 세계, 정말 λ§€λ ₯적이지 μ•Šλ‚˜μš”? 저도 이번 κΈ°νšŒμ— νƒ€μž… μ‹œμŠ€ν…œμ— λŒ€ν•΄ 더 깊이 곡뢀할 수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 이 글이 μ—¬λŸ¬λΆ„μ˜ νƒ€μž…μŠ€ν¬λ¦½νŠΈ 여정에 μž‘μ€ 도움이 λ˜μ—ˆμœΌλ©΄ μ’‹κ² μŠ΅λ‹ˆλ‹€.