타입스크립트의 타이핑
- 자바스크립트는 덕 타이핑(duck typing) 기반이고 타입스크립트가 이를 모델링하기 위해
구조적 타이핑을 사용합니다.
- 타입스크립트가 구조적 타이핑을 도입한 이유는 동적 타입 언어인 자바스크립트를 기반으로 하기 때문입니다.
- Java 기반의 객체지향에서는
명목적 타이핑을 사용합니다.
구조적 타이핑이란?
- 구조적 타이핑은 값 자체의 타입보다는 값이 가진
내부 구조에 기반해서 타입 호환성을 검사한다.
- 좋든 싫든 타입은 열려있습니다.
- 타입스크립트의 클래스 역시 구조적 타이핑 규칙을 따릅니다.
- 클래스의 인스턴스가 Java 기반의 객체지향과 다를 수 있습니다.
- 어떤 인터페이스에 할당 가능한 값이라면 타입 선언에 명시적으로 나열된 속성들을 가지고 있을 겁니다. 타입은
봉인되어 있지 않습니다.
- 추가 속성이 있으면 값의 집합은 더 작아집니다.
- 반대로 유니온 타입이 있으면 값의 집합은 더 커집니다.
내부 구조만 같다면 타입은 동일한 것으로 판단합니다.
class ClassA {
str: string;
constructor(str: string) {
this.str = str;
}
}
const classA = new ClassA('instance of A');
const objectB: ClassA = { str: 'object literal' };
class A {
clap() {
console.log('A');
}
}
class B {
clap() {
console.log('B');
}
}
const C = {
clap() {
console.log('C');
},
};
let a: A = new A();
a.clap();
a = new B();
a.clap();
a = C;
a.clap();
a = { clap: () => console.log('D') };
a.clap();
인터페이스의 이름이 달라도 내부 구조가 같다면 할당 가능합니다.
이는 Java 기반의 객체지향과의 큰 차이점이며 우리가 구조적 타이핑을 잘 이해하고 사용해야 하는 이유 입니다.
interface Vector1D {
x: number;
}
interface Point {
x: number;
}
let p = v1;
v1 = p;
타입스크립트는 구조적 타이핑(덕 타이핑)을 사용하기 때문에, 값을 세밀하게 구분하지 못하는 경우가 있습니다
- 값을 구분하기 위해 공식 명칭이 필요하다면 상표를 붙이는 것을 고려해야 합니다.
- 상표 기법은 타입 시스템에서 동작하지만 런타임에 상표를 검사하는 것과 동일한 효과를 얻을 수 있습니다.
type Meters = number & { _brand: 'meters' };
type Seconds = number & { _brand: 'seconds' };
const meters = (m: number) => m as Meters;
const seconds = (s: number) => s as Seconds;
const oneKm = meters(1000);
const oneMin = seconds(60);
상속을 받은 것과 나열한 것 역시 내부 구조가 같다면 동일한 것으로 판단 합니다.
interface Vector1D {
x: number;
}
interface Vector2D extends Vector1D {
y: number;
}
interface Vector2D2 {
x: number;
y: number;
}
interface Vector3D extends Vector2D {
z: number;
}
interface Vector3D2 {
x: number;
y: number;
z: number;
}
구조적 타이핑은 잉여 속성 체크에 한계가 있습니다.
객체 리터럴 변수에 할당하거나 함수에 매개변수로 전달할 때 잉여 속성 체크가 수행됩니다.
- 잉여 속성 체크는 오류를 찾는 효과적인 방법이지만, 타입스크립트 타입 체커가 수행하는 일반적인 구조적 할당 가능성 체크와 역할이 다릅니다. 할당의 개념을 정확히 알아야 잉여 속성 체크와 일반적인 구조적 할당 가능성 체크를 구분할 수 있습니다.
- 잉여 속성 체크는 구조적 타이핑 시스템에서 허용되는 속성 이름의 오타 같은 실수를 잡는 데 효과적인 방법입니다. 선택적 필드를 포함하는 타입에 특히 유용한 반면, 적용 범위도 매우 제한적이며
오직 객체 리터럴에만 적용됩니다.
- 잉여 속성 체크에는 한계가 있습니다.
임시 변수를 도입하면 잉여 속성 체크를 건너뛸 수 있다는 점을 기억해야 합니다.
interface Vector1D {
x: number;
}
interface Vector2D extends Vector1D {
y: number;
}
let v1: Vector1D = {
x: 1,
};
let v2: Vector2D = {
x: 1,
y: 2,
};
v1 = v2;
v2 = v1;
interface A {
a: string;
b: string;
}
const a: A = {
a: 'a',
b: 'b',
c: 'c',
};
const b = {
a: 'a',
b: 'b',
c: 'c',
};
const c: A = b;
그렇다면 타입을 어떻게 사용해야 할까요?
- 공통적인 것을 원본 모델로 만들고 이를 유틸리티 타입을 활용해서 파생된 타입을 사용합니다.
- 타입스크립트는
절충하는 언어입니다.
- 원본 타입과 유틸타입의 조합으로 타입이 많아지는 것을 방지합니다.
리액트에서 컴포넌트를 만드는 것처럼 타이핑을 해보겠습니다.
- 원본 모델(인터페이스)과 타입 레이어의 분리합니다.
- interface는 병합이 가능하다는 특징을 활용해
외부와의 소통에 활용합니다. 사용하는 곳에서 필요에 따라 유틸리티 타입 혹은 병합 성질을 활용해서 사용합니다.
- type은 파일 내에서 interface를 활용해서 만들어서 사용합니다.
- 원본 모델은 css-in-js나 custom hook에서 거의 같지만 조금 다르게 활용되는 경우가 많습니다. 보통 Component의 props에서는 optional인 값이 css-in-js로 넘어갈 때는 필수값이 됩니다. 이를 위해 타입을 2번 만드는 것이 아니라 원본 모델과 유틸리티 타입을 활용해 타입을 여러개 만들지 않고 유지보수성도 향상 시킬 수 있습니다.
export interface TextProps {
value: string;
placeholder: string;
color: string;
border: string;
}
type TextPropsMustBe = Pick<TextProps, 'value' | 'border'>;
type TextPropsPartialType = Partial<TextProps> & TextPropsMustBe;
type TextContainerProps = Pick<TextProps, 'color'>;
const TextComponent: (props: TextPropsPartialType) => string = (props) => {
const passedProps = useText(props);
return TextContainer(passedProps);
};
const TextContainer = (props: TextContainerProps) => `
color: ${props.color};
`;
function useText(
props: TextPropsPartialType,
): TextPropsMustBe & TextContainerProps {
return {
color: props.value || '#fff',
value: props.value,
border: props.border,
};
}
TextComponent({ value: '', border: '1px solid black' });
참고
- 마광휘님의 설명
- 타입스크립트의 핵심 개념인 구조적 타입 시스템을 기반으로 설명하면, 타입스크립트에서 구조적 타입 시스템을 선택한 이유는 다음과 같습니다. interface는 가장 기저에 있는 객체의 모델을 나타냅니다. 다만, 자바스크립트는 타입으로 정의할 수 없는 다양한 형태가 런타임으로 동작될 수 있습니다. 그렇기에 런타임과 컴파일 타임 (사실 타입 체킹 타임 이겠죠?)이 다르고, 런타임에서 원하는 (허용해야하는) 경우와 오브젝트에서 은닉하고 캡슐화 해야하는 경우 등이 존재. 그러한 다양한 타입을 포용하려면 타입을 자유롭게 가공하고 만들 수 있도록 제공되어야 했습니다. 그렇기에 다양한 Utility Type과 그에따른 순수 객체를 위한 인터페이스를 분리하고 조합하여 Type Alias를 하여금 더 복잡한 타입을 나타낼 수 있도록 하였고, 궁극적인 지향점은, 아주 최소한의 모델을 만들고 최소한의 모델을 유연하게 타입으로 만들어서, 타입을 조합해 최소한의 타입으로 동작하도록 하는 것입니다. 또한 이 구조는 타입스크립트에서 지향하고 있는 “점진적 타이핑“과 자바스크립트의 “덕 타이핑” 관점에서도 충분히 먹히는 선택지기에 가게 되었죠. (출처: 타입스크립트 위키피디아 + 타입스크립트 깃헙 MR)
서브타입 파보기
extends에서의 관계
서브셋이 아닌 경우
interface A {
a: string;
}
interface B extends A {
a: number;
}
서브셋인 경우
interface A {
a?: string;
}
interface B extends A {
a: string;
}
서브셋이 아닌 경우2
interface A {
a: string;
}
interface B extends A {
a?: string;
}
선언 병합에서는?
같은 타입인 경우
interface A {
a: string;
}
interface A {
a: string;
}
서브셋이 아닌 경우
interface A {
a: string;
}
interface A {
a: number;
}
서브셋인 경우
interface A {
a: string;
}
interface A {
a?: string;
}
서브셋인 경우2
interface A {
a: string;
}
interface A {
a: 'a';
}
집합론적으로 타입 바라보기
구조적 타이핑이란? 내용을 다시 한번 읽고 보시면 좋습니다.
- 타입을 공변의 입장에서 바라봅니다.
- 타입을 개념으로 바라보는지 원소로 바라보는지에 따라서도 차이가 있습니다.
number는 숫자의 슈퍼 타입이다.
const a: number = 1;
const b: 1 = a;
const c: 1 = 1;
const d: number = c;
readonly Array는 Array의 슈퍼 타입이다.
const a: readonly [1] = [1];
const b: [1] = a;
const c: [1] = [1];
const d: readonly [1] = c;
객체를 인자로 넘기는 경우는 어떤가
function a(obj: { b: string }) {
return obj;
}
const c = {
b: '1',
};
a(c);
const d = {
b: '1',
e: '1',
};
a(c);
a({ b: '1' });
a({ b: '1', e: '1' });
- 일반적으로 객체에 속성이 더 있다면 잉여 속성을 체크하지 않아서 문제가 없습니다. 하지만
함수에 매개변수로 전달할 때 잉여 속성 체크가 수행됩니다. 이것은 구조적 타이핑의 한계를 제한하기 위해 도입된 것이므로 구조적 타이핑 입장에서의 집합론에서는 잉여 속성이 있어도 서브 타입으로 보는 것이 맞습니다.
인터페이스로 바라보기
interface A {
a: string | undefined;
}
interface B extends A {
a: string;
}
interface C extends A {
a: undefined;
}
interface D {
a: undefined;
}
interface E extends D {
a: string;
}
interface F {
a: string;
b: string;
}
type G = Omit<F, 'a'>;
let f: F = {
a: 'a',
b: 'b',
};
let g: G = {
b: 'b',
};
g = f;
f = g;
interface H {
a: string;
}
interface I {
b: string;
}
type J = H & I;
const h: H = {
a: 'a',
};
const i: I = {
b: 'b',
};
interface J {
c: string;
}
interface H extends J {
a: string;
}
interface I extends J {
b: string;
}
집합이란?