반응형
keyof 연산자
keyof를 이용하면 객체의 모든 키를 문자열 리터럴 타입 유니온으로 얻을 수 있습니다.
다음은 APIResponse에 적용한 예입니다.
type APIResponse = {
user: {
userId: string;
friendList: {
count: number;
friends: {
firstName: string;
lastName: string;
};
};
};
};
type ResponseKeys = keyof APIResponse // type ResponseKeys = "user"
type UserKeys = keyof APIResponse['user'] // type UserKeys = "friendList" | "userId"
type FriendListKeys = keyof APIResponse['user']['friendList'] // type FriendListKeys = "friends" | "count"
키인과 keyof 연산자를 혼합해 사용하면 객체에서 주어진 키에 해당하는 값을 반환하는 게터를 타입 안전한 방식으로 구현할 수 있습니다.
function get< // ①
O extends object,
K extends keyof O // ②
>(
o: O,
k: K
): O[K]{ // ③
return o[k]
}
- get은 객체 o와 키 k를 인수로 받는 함수입니다.
- keyof O는 문자열 리터럴 타입의 유니온으로 o의 모든 키를 표현합니다. 제네릭 타입 K는 그 유니온을 상속받습니다.(따라서 이 유니온의 서브 타입입니다.) 예를 들어 o가 {a: number, b: string, c: boolean}이라면 keyof o는 'a' | 'b' | 'c' 타입이 되며 keyof o를 상속받은 K는 'a', 'b', 'a' | 'c' 등 keyof o의 서브 타입이 될 수 있습니다.
- O[K]는 O에서 K를 찾을 때 얻는 타입입니다. 2에서 든 예로 계속 설명하자면, K가 'a'라면 컴파일 타임에 get이 number를 반환하고, K가 'b' | 'c'라면 get은 string | boolean을 반환합니다.
안전하게 타입의 형태를 묘사할 수 있다는 부분이 이 타입 연산자들의 멋진 점입니다.
type ActivityLog = {
lastEvent: Date
events: {
id: string
timestamp: Date
type: 'Read' | 'Write'
}[]
}
let activityLog: ActivityLog = // 블라 블라
let lastEvent = get(activityLog, 'lastEvent') // Date
TypeScript는 컴파일 타임에 lastEvent의 타입이 Date라는 사실을 파악합니다.
물론 이를 확장해 객체에 더욱 깊숙이 키인 할 수 있습니다.
키를 세 개까지 받을 수 있도록 get을 오버로드합시다.
type ActivityLog = {
lastEvent: Date
events: {
id: string
timestamp: Date
type: 'Read' | 'Write'
}[]
}
let activityLog: ActivityLog = // 블라 블라
let lastEvent = get(activityLog, 'lastEvent') // Date
type Get = { // ①
<O extends object,
K1 extends keyof O
>(o: O, k1: K1): O[K1] // ②
<
O extends object,
K1 extends keyof O,
K2 extends keyof O[K1] // ③
>(o: O, k1: K1, k2: K2): O[K1][K2] // ④
<
O extends object,
K1 extends keyof O,
K2 extends keyof O[K1],
K3 extends keyof O[K1][K2]
>(o: O, k1: K1, k2: K2, k3: K3): O[K1][K2][K3] // ⑤
}
let get: Get =(object: any, ...keys: string[]) => {
let result = object
keys.forEach(k => result = result[k])
return result
}
get(activityLog, 'events', 0, 'type') // "Read" | "Write" (+2 overloads)
get(activityLog, 'bad') // '"bad"' 형식의 인수는 'keyof ActivityLog' 형식의 매개 변수에 할당될 수 없습니다.ts(2345)
- get을 한 개, 두 개, 세 개의 키로 호출할 수 있도록 get의 함수 시그니처 선언을 오버로드했습니다.
- 이전 마지막 예제처럼 한 개의 키를 갖는 상황입니다. 즉 O는 객체의 서브 타입이며 K1은 객체 키의 서브 타입이고, O에 K1으로 키인 할 때 얻는 타입이 반환 타입입니다.
- 두 개의 키를 갖는 상황은 한 개의 키를 갖는 상황과 거의 같습니다. 다만 제네릭 타입 K2를 추가로 선언함으로 O에 K1으로 키인 해서 얻는 중첩 객체에 존재할 수 있는 키를 표현합니다.
- 키인을 두 번 수행하여 2를 확장합니다. 먼저 O[K1]의 타입을 얻은 다음, 그 결과에서 [K2]의 타입을 얻습니다.
- 이 예제에서는 중첩 키를 세 개까지 처리할 수 있습니다. 실제 라이브러리를 구현할 때는 이보다 많은 키를 처리해야 할 수도 있을 것입니다.
TSC 플래그: keyofStringsOnly
JavaScript에서 객체와 배열 모두 문자열과 심벌 키를 가질 수 있습니다. 다만 배열엔 보통 숫자 키를 쓰는 것이 규칙인데 런타임에 숫자 키는 문자열로 강제 변환됩니다.
이런 이유로 TypeScript의 keyof는 기본적으로 number | string | symbol 타입의 값을 반환합니다.
올바른 동작이지만 이 때문에 TypeScript에게 특정 키가 string이고 number나 symbol이 아니라는 사실을 증명해야 하는 귀찮은 상황에 놓일 수 있습니다.
TypeScript가 예전처럼 동작하길 원한다면 tsconfig.json에서 keyofStringsOnly 플래그를 활성화하면 됩니다:)
반응형
'👶 TypeScript' 카테고리의 다른 글
매핑된 타입(mapped type) (0) | 2023.01.20 |
---|---|
Record 타입 (0) | 2023.01.20 |
객체 타입의 타입 연산자 - 키인 연산자 (0) | 2023.01.19 |
철저 검사(Exhaustiveness Checking)라 불리는 종합성(Totality) (0) | 2023.01.19 |
정제 - 차별된 유니온 타입 (0) | 2023.01.19 |