한정된 다형성
이번엔 이진트리 예제를 사용합니다.
기본적으로 이진트리의 특징은 아래와 같습니다.
- 이진트리는 자료구조입니다.
- 이진트리는 노드를 갖습니다.
- 노드의 값을 가지며 최대 두 개의 자식 노드를 가리킬 수 있습니다.
- 노드는 잎 노드(leaf node: 자식이 없음) 또는 내부 노드(inner node: 적어도 한 개의 자식을 가짐) 둘 중 하나의 타입을 갖습니다.
"T는 제네릭 타입이며, 이것은 T와 같은 타입이어야 한다"는 말로 표현할 수 없는 상황이 많습니다.
때론 U 타입은 적어도 T 타입을 포함하는 기능이 필요합니다. 이러한 상황을 U가 T의 상한 한계(upper bound)라고 설명합니다.
아래와 같은 세 종류의 노드를 갖는 이진트리를 구현한다고 해보겠습니다.
- 일반 TreeNode
- 자식을 갖지 않는 TreeNode인 LeafNode
- 자식을 갖는 TreeNode인 InnerNode
먼저 각 노드의 타입을 선언합니다.
type TreeNode = {
value: string
}
type LeafNode = TreeNode & {
isLeaf: true
}
type InnerNode = TreeNode & {
children: [TreeNode] | [TreeNode, TreeNode]
}
TreeNode는 value라는 한 개의 프로퍼티만 갖는 객체라고 정의했습니다.
LeafNode 타입은 TreeNode가 갖는 모든 프로퍼티뿐 아니라 값이 항상 true인 isLeaf 프로퍼티를 추가로 포함합니다.
InnerNode도 TreeNode의 모든 프로퍼티를 포함하며 한 개나 두 개의 자식을 가리킬 수 있는 children 프로퍼티를 추가로 포함합니다.
다음으로 TreeNode를 인수로 받아 value에 매핑 함수를 적용해 새로운 TreeNode를 반환하는 mapNode 함수를 구현해 보겠습니다.
아래처럼 사용할 수 있는 mapNode 함수가 필요하다고 가정해 봅니다.
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]}
let a1 = mapNode(a, _ => _.toUpperCase()) // TreeNode
let b1 = mapNode(b, _ => _.toUpperCase()) // LeafNode
let c1 = mapNode(c, _ => _.toUpperCase()) // InnerNode
TreeNode의 서브타입을 인수로 받아 같은 서브타입을 반환하는 mapNode 함수를 구현하는 방법은 아래와 같습니다.
function mapNode<T extends TreeNode>( // ①
node: T, // ②
f: (value: string) => string
): T{ // ③
return {
...node,
value: f(node.value)
}
}
① mapNode는 한 개의 제네릭 타입 매개변수 T를 정의하는 함수입니다. T의 상한 경계는 TreeNode입니다. 즉, T는 TreeNode이거나 아니면 TreeNode의 서브타입입니다.
② mapNode는 두 개의 매개 변수를 받는데, 첫 번째 매개변수는 T 타입의 노드입니다. ①에서 노드는 extends TreeNode라고 했으므로 빈 객체 {}, null, TreeNode 배열 등의 TreeNode가 아닌 다른 것을 인수로 전달하면 바로 빨간 밑줄이 나타납니다. node는 TreeNode이거나 TreeNode의 서브타입이어야 합니다.
③ mapNode는 타입이 T인 값을 반환합니다. T는 TreeNode이거나 TreeNode의 하위 타입임을 기억해야 합니다.
T를 위의 방식으로 선언하는 이유는 다음과 같습니다.
- extends TreeNode를 생략하고 T 타입을 그저 T라고 쓰면, 특정 타입과 연결되지 않은 mapNode가 컴파일 타임 에러를 던집니다. T 타입엔 상한 경계가 없으므로 node.value를 읽는 행위가 안전하지 않기 때문입니다.
- T를 아예 사용하지 않고 mapNode를 (node: TreeNode, f: (value: string) => string) => TreeNode처럼 선언하면 매핑되면서 타입 정보가 날아가서 a1, b1, c1이 모두 TreeNode가 됩니다.
T extends TreeNode라고 표현함으로써 매핑한 이후에도 입력 노드가 특정 타입(TreeNode, LeafNode, InnerNode)이라는 정보를 보존할 수 있습니다.
'👶 TypeScript' 카테고리의 다른 글
한정된 다형성으로 인수의 개수 정의하기 (0) | 2023.01.14 |
---|---|
여러 제한을 적용한 한정된 다형성 (0) | 2023.01.13 |
제네릭 타입 별칭 (0) | 2023.01.13 |
제네릭 타입 추론 (0) | 2023.01.13 |
제네릭을 어디에 선언할 수 있을까? (0) | 2023.01.13 |