Post

TypeScript 의 상속과 브랜딩

이번에 다루어 볼 것은, 타입의 상속과 브랜딩이다.

상속

자바스크립트에서는 객체 간의 상속이 가능하다. 상속을 하면 부모 객체에 존재하는 속성을 다시 입력하지 않아도 되므로 중복 제거가 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Animal {
    constructor(name) {
        this.name = name;
    }
}

class Dog extends Animal {
    bark() {
        console.log(`${this.name} 멍멍`)
    }
}

class Cat extends Animal {
    meow() {
        console.log(`${this.name} 야옹`)
    }
}

클래스 Animal 을 상속하는 Dog, Cat 은 name 속성을 선언해주지 않아도 extends 키워드를 통한 상속으로 name 속성을 갖게 만들 수 있다. 이렇게 생성한 Dog, Cat 의 name 속성은 this.name 을 통해 접근이 가능하다.

타입 별칭에서도 상속과 같은 효과를 낼 수 있는데, 아래 예시를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type Animal = {
    name: string;
}

type Dog = Animal & {
    bark(): void;
}

type Cat = Animal & {
    meow(): void;
}

type Name = Cat['name'];

type Dog = Animal | {
    bark(): void;
}

type Cat = Animal | {
    meow(): void;
}

type Name = Cat['name']; // Property 'name' does not exist on type 'Cat'.

type D = { a: 'b'} & { b : 'c'};

const d: D = {a: 'b', b: 'n'}; // Type '"n"' is not assignable to type '"c"'.
const d: D = {a: 'b', b: 'c'}; 

& 연산자를 이용하면 상속을 나타낼 수 있다. | 연산자를 사용해도 문제가 없지 않냐고 할지도 모르나, 둘 중 아무 것이나 오면 된다는 것은 상속을 구현하길 원한다면 전혀 다른 개념이기 때문에 유의해야한다.

추가로 다뤄볼 것이 있는데, 상속을 할 때 부모로 부터 상속받은 속성의 타입을 변경할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Merge {
    one: string;
    two: string;
}

interface Merge2 extends Merge {
    one: 'h' | 'w';
    two: '123';
}

interface Merge3 extends Merge {
    one: 'h' | 'w';
    two: 123; 
    // Interface 'Merge3' incorrectly extends interface 'Merge'.
    // Types of property 'two' are incompatible.
    // Type 'number' is not assignable to type 'string'.
}

물론, 부모 속성의 기존 타입과 완전히 다른 타입으로 변경하는 경우는 제한하고 있다. 타입을 보다 더 좁히는 것만 가능하게 되어있으니, 참고하도록 하자.

구조적 타이핑과 브랜딩

이전 게시글에서, & 연산자를 다루면서 ‘브랜딩’ 을 언급한 적이 있다.

1
type H = { a: 'b' } & number; // H = { a: 'b' } & number

바로 이 상황이었는데, 이런 식으로 선언을 하더라도 타입 H 는 never 가 되지 않았다.

구조적 타이핑에 대해 이해한다면, 어떤 상황에서 유용하게 사용할 수 있는지 알 수 있게 된다.

1
2
3
4
5
6
7
8
9
interface A {
    name: string;
    age: number;
}

interface B {
    name: stirng;
    age: number;
}

이렇게 선언을 한 경우, 타입스크립트가 두 인터페이스 A 와 B 를 동일한 타입으로 취급한다. 조금 더 실질적인 예시를 추가해보자.

1
2
3
4
5
6
7
8
9
10
11
12
interface Money {
    amount: number;
    unit: string;
}

interface Liter {
    amount: number;
    unit: string;
}

const liter: Liter = { amount: 1, unit: 'liter' };
const money: Money = liter;

두 객체가 속성과 그 타입이 동일하지만, 서로 다른 데이터를 가지게 될 경우엔 구분을 하고 싶을 수도 있다. 하지만, 이 예시 처럼 타입스크립트는 두 타입이 같은 속성을 갖기에 같은 타입으로 취급한다.

이럴 때, 서로 다른 타입임을 구분하기 위해 사용하는게 바로 브랜딩이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Money {
    __type: 'money';
    amount: number;
    unit: string;
}

interface Liter {
    __type: 'liter';
    amount: number;
    unit: string;
}

const liter: Liter = { __type:'liter', amount: 1, unit: 'liter' };
const money: Money = liter; 
    // Type 'Liter' is not assignable to type 'Money'.
    // Types of property '__type' are incompatible.
    // Type '"liter"' is not assignable to type '"money"'.

각각 속성에 __type 이라는 별개의 속성을 추가해 구분해주었다. 서로 다른 타입임을 구분해주는 __type 을 ‘브랜드(brand)’ 라고 한다. 따라서, 브랜드를 사용하는 걸 브랜딩이라고 할 수 있겠다.

다만 속성 이름을 반드시 __type 으로 사용할 필요는 없고, 컨벤션으로 보이니 참고하자.

This post is licensed under CC BY 4.0 by the author.