Front-End/TypeScript

타입스크립트[TypeScript] | 제네릭(Generic) 제약조건 설정하기

jaeyeong 2023. 4. 24. 15:26

🔍 글을 시작하기 전에

 

해당 글은 타입스크립트 프로그래밍 책과 유데미 타입스크립트 강의를 공부하여 정리한 글입니다.

수정해야 할 부분이 있다면 언제든지 알려주세요!

T extends type

전달받은 두 개의 객체를 하나로 합치는 함수를 구현해 보자.

function merge<T, U>(objA: T, objB: U) {
    return Object.assign(objA, objB);
}

 

코드 실행은 잘 되지만, 에러가 발생하는 것을 확인할 수 있었다.

에러는 return 문에서 assign메서드에 전달되는 objA에서 발생했는데, 다음과 같은 오류가 발생했다.

(더 보기를 누르면 발생한 오류 문구를 확인할 수 있어요.)

더보기

No overload matches this call.

Overload 1 of 4, '(target: {}, source: U): {} & U', gave the following error. Argument of type 'T' is not assignable to parameter of type '{}'.

Overload 2 of 4, '(target: object, ...sources: any[]): any', gave the following error. Argument of type 'T' is not assignable to parameter of type 'object'.

 

이는 Object.assign() 함수에서 첫 번째 매개변수로 전달되는 objAobject 타입이 아니라서 발생하는 오류였다.

오류를 해결하기 위해서는 첫 번째 매개변수인 objA 타입에 object가 온다는 것을 명시해줘야 했다.

extends 키워드를 사용하여 오류를 해결했다.

function merge<T extends object, U extends object>(objA: T, objB: U) {
    return Object.assign(objA, objB);
}

let objMerge = merge({name: 'Jae'}, {age: 27});
console.log(objMerge); // {"name": "Jae",  "age": 27}

 

T extends object를 사용함으로써 T 타입이 어떤 구조를 가지는 객체인지는 상관없이,

일단 타입이 객체여야 한다는 의미를 가지게 된다.

 

하나의 제한을 적용한 한정된 제네릭

U타입은 적어도 T타입을 포함하고 있다는 것을 U가 T의 상한 한계다라고 한다.

type TreeNode = {
    value: string
}

type LeafNode = TreeNode & {
    isLeaf: true
}

type InnerNode = TreeNode & {
    children: [TreeNode] | [TreeNode, TreeNode]
}

let a: TreeNode = {value: 'a'};
let b: LeafNode = {value: 'b', isLeaf: true};
let c: InnerNode = {value: 'c', children: [b]};

 

이런 이진트리가 있을 때, node를 인수로 받아 value에 매핑 함수를 적용해서 새로운 node를 반환하는

mapNode 함수를 구현한다고 했을 때, mapNode 함수는 다음과 같이 작성할 수 있다.

function mapNode<T extends TreeNode>(node: T, f: (value: string) => string): T {
    return {
        ...node,
        value: f(node.value)
    }
}

 

그리고 LeafNode 타입인 b를 매핑 함수 인자에 전달한 뒤 타입을 확인해 보자.

let b1 = mapNode(b, _ => _.toUpperCase()); // { "value": "B", "isLeaf": true }
// b1의 타입: LeafNode

 

b1에 마우스를 올려보면 타입 정보가 보존되어 여전히 LeafNode 타입이다.

만약 T extends TreeNode를 지우고 T라고 작성하면 어떻게 될까?

function mapNode<T>(node: T, f: (value: string) => string): T {
    return {
        ...node,
        value: f(node.value) //Error: Property 'value' does not exist on type 'T'.
    }
}

 

T에 value 속성이 없다는 오류가 발생하게 된다.

T 타입에 상한 경계가 없기 때문에 node.value를 읽으려고 할 때 안전하지 않아 오류가 발생하게 되는 것이다.

 

그렇다면 제네릭을 사용하지 않고 T대신 그냥 TreeNode를 사용한다면 어떻게 될까?

function mapNode(node: TreeNode, f: (value: string) => string): TreeNode {
    return {
        ...node,
        value: f(node.value)
    }
}

let b1 = mapNode(b, _ => _.toUpperCase()); // { "value": "B", "isLeaf": true }
// b1의 타입: TreeNode

 

b1의 값은 동일하게 반환되지만, b1의 타입이 TreeNode로 바뀌게 된다.

즉, b 노드가 매핑 함수를 통해 매핑되면서 타입 정보가 날아가게 되는 것이다.

 

T extends TreeNode로 타입을 표기함으로써 매핑한 이후에도 사용된 노트의 특정 타입을 보존할 수 있게 된다.

 

타입 별칭에서 제네릭 활용하기

타입 별칭에서 제네릭을 활용할 경우 타입이 자동으로 추론되지 않기 때문에 타입 매개변수를 명시적으로 한정해야 한다.

type MyEvent<T> = {
    target: T
    type: string
}

let myEvent: MyEvent<HTMLButtonElement | null> = {
    target: document.querySelector('#myButton'),
    type: 'click'
}