Typescript

[Typescript] 타입은 값들의 집합

prefer2 2022. 9. 18. 16:48

 

 

 

 

 

 

interface Person {
  name: string;
}

interface Lifespan {
  birth: Date;
  death?: Date;
}

type PersonSpan = Person & Lifespan;
// PersonSpan = {
// 	name: string;
//	birth: Date;
//	death?: Date;
// }

타입스크립트를 처음 접했을 때 가장 이해가 가지 않았던 부분은 교집합이었다. 일반적인 관점으로 바라보면 Person과 Lifespan 집합에는 교집합이 없어보인다. 그래서 결론으로 never(공집합)이 나오지 않을까라고 예측하기 쉽다. 하지만 결과는 두 집합의 모든 속성들을 가지는 집합이 나오게 된다. 이를 이해하기 위해서는 구조적 타이핑을 먼저 이해해야 한다. 

 

 

구조적 타이핑


자바스크립트의 덕 타이핑(duck typing)을 모델링 하기 위해 타입스크립트는 구조적 타이핑을 사용해야 한다. 

“만약 어떤 새가 오리처럼 걷고, 헤엄치고, 꽥꽥거리는 소리를 낸다면 나는 그 새를 오리라고 부를 것이다.” 🦆

덕 타이핑은 객체가 어떤 타입에 부합하는 최소한의 특징을 가지고 있다면, 그 타입에 해당하는 것이라고 간주하는 것이다. 

 

interface Vector2D {
  x: number;
  y: number;
}

interface NamedVector {
  name: string;
  x: number;
  y: number;
}

function calculateLength(v: Vector2D){
  return Math.sqrt(v.x * v.x + v.y * v.y)
}

const v: NamedVector = { x:3, y:4, name:'name' };
console.log(calculateLength(v)); // 5, 정상 동작

 

NamedVector에는 name이라는 추가적인 속성을 가지고 있지만 calculateLength에서 파라미터 타입 체커를 통과한다. v는 x 속성도 가지고 있고, y 속성도 가지고 있기 때문이다.

구조적 타이핑과 덕타이핑의 차이는 타입 체커가 동작하는 시점에 있다. 덕 타이핑은 런타임에 접근할 수 있는 타입의 구조를 기준으로 한다. 반면, 구조적 타이핑은 컴파일 타임에 타입의 구조에 따라 호환성과 동일성을 비교한다.

 

 

타입을 집합의 관점에서 바라보기


교집합(인터섹션, &)

타입은 할당 가능한 값들의 집합이라고 생각하면 된다. 이 집합은 타입의 범위라고 부르기도 한다.

interface A {
  x: number;
  y: number;
}

interface B {
  z: string
}

type AB = A & B;

다시 교집합(&)을 생각해보자. 교집합이 되려면 A로 간주해도, B로 간주해도 무리가 없어야 한다. 구조적 타이핑과 이를 연결하면 A에 있는 속성도 가지고 있으면서, B에 있는 속성들도 모두 가지고 있어야 A로 간주해도, B로 간주해도 문제가 없다. 이것이 typescript 관점에서의 교집합이다.

 

합집합(유니온, |)

interface A {
  x: number;
  y: number;
}

interface B {
  z: string
}

type AB = keyof( A | B ); // never

유니온은 A에도 속해야하고 B에도 속해야 하는 속성이다. A타입이면서 B타입인 것은 불가능하다. 따라서 keyof의 결과가 never이다. 

 

유니온 타입에서는 keyof를 사용하는 이유

유니온은 집합의 관점에서 합집합이다. 위 예시에서 keyof없이 생각해보면 구조적 타이핑의 개념에서 아래와 같은 일들이 가능하다. A 집합이거나 B 집합이라는 의미이기 때문에 개별 원소로 분리할 수 없다. 

type AB =  A | B; 

const C = {
  x: 1,
  y: 2,
  a: 'hello'
}

const D  = {
  z: 'z'
}

function isAB(ab: AB) {
  console.log('안녕하세요 AB 타입입니다')
}

isAB(C) // 안녕하세요 AB 타입입니다
isAB(D) // 안녕하세요 AB 타입입니다

 

 

keyof (A&B) = (keyof A) | (keyof B) // x, y, z
keyof (A|B) = (keyof A) & (keyof B) // never

이제 이 등식이 이해 될 것이다.

 

extends

구조적 타이핑의 개념에서 extends를 바라보자. 구조적 타이핑의 개념에서 A의 집합은 무한하다. a속성만 가지고 있다면 무엇이든 A 타입이 될 수 있다. 이 관점에서 AB를 바라보면 당연하게 AB는 A타입이 될 수 있다.

interface A { a: number; }
interface AB extends A { b: number; }
interface ABC extends AB { c: number; }

 

 

 

+) type과 interface를 선택한 기준


프로젝트를 진행하며 type과 interface간의 선택 기준을 세워야 하는 일이 있었다. 이 과정에서 interface는 객체 타입임을 명시해주는 것 말고는 장점이 없을까라는 생각이 들었다. 객체 타입임을 명시해 줄 수 있다는 장점만 있다면 선언 병합을 막아주는 type을 사용하고 interface는 사용하지 않는 편이 오히려 편하지 않을까라고 생각했다. 이 글을 작성하며 intersection과 extends에 대해 다시 생각해보고 interface의 필요성을 느꼈다.

type type1 = { a: 1 } & { b: 2 } // ok
type type2 = { a: 1; b: 2 } & { b: 3 } // resolved to `never`

interface inter {
  b: 3
}

interface inter2 extends inter {
  a: 1,
  b: 2,
} // 타입 선언시에 문제가 발생함

const t2: type1 = { a: 1, b: 2 } // good
const t3: type2 = { a: 1, b: 3 } // Type 'number' is not assignable to type 'never'
// 사용시에 문제가 발생함 -> 디버깅 어려워질수도

type으로 선언하고 intersection할 시 기대했던 바와 다르게 작동한다. type2는 b속성에 의해 never타입이 된다. interface를 사용하여 extends 할 시에는 타입 선언에서 문제가 발생한다. type으로 선언시에는 선언시에 문제를 발견할 수 없고 사용시에 문제가 발생해 디버깅에 어려움을 느낄 수 있겠다는 결론을 내려 literal type의 union이 아닌 경우에는 interface를 사용하기로 결정했다.

 

공식 문서에서도 다음과 같이 안내하고 있다

The principle difference between the two is how conflicts are handled, and that difference is typically one of the main reasons why you’d pick one over the other between an interface and a type alias of an intersection type.
If you would like a heuristic, use interface until you need to use features from type.

 

타입스크립트 컨벤션을 포함한 추가적인 컨벤션은 위키에 정리해두고 맞춰나갔다.

네이밍에 ~props, ~params를 붙여 어떤 특징을 가지고 있는지 정리하고자 했으며, React의 propsWithChildren과 EventHandler 타입을 사용하는 것으로 했다.

 

참조

http://blog.hwahae.co.kr/all/tech/tech-tech/9954/

https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#differences-between-type-aliases-and-interfaces

반응형