👶 TypeScript

객체 타입의 타입 연산자 - keyof 연산자

개발자 린다씨 2023. 1. 19. 18:00
반응형

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]
    }
  1. get은 객체 o와 키 k를 인수로 받는 함수입니다.
  2. 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의 서브 타입이 될 수 있습니다.
  3. 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)
  1. get을 한 개, 두 개, 세 개의 키로 호출할 수 있도록 get의 함수 시그니처 선언을 오버로드했습니다.
  2. 이전 마지막 예제처럼 한 개의 키를 갖는 상황입니다. 즉 O는 객체의 서브 타입이며 K1은 객체 키의 서브 타입이고, O에 K1으로 키인 할 때 얻는 타입이 반환 타입입니다.
  3. 두 개의 키를 갖는 상황은 한 개의 키를 갖는 상황과 거의 같습니다. 다만 제네릭 타입 K2를 추가로 선언함으로 O에 K1으로 키인 해서 얻는 중첩 객체에 존재할 수 있는 키를 표현합니다.
  4. 키인을 두 번 수행하여 2를 확장합니다. 먼저 O[K1]의 타입을 얻은 다음, 그 결과에서 [K2]의 타입을 얻습니다.
  5. 이 예제에서는 중첩 키를 세 개까지 처리할 수 있습니다. 실제 라이브러리를 구현할 때는 이보다 많은 키를 처리해야 할 수도 있을 것입니다.

TSC 플래그: keyofStringsOnly

JavaScript에서 객체와 배열 모두 문자열과 심벌 키를 가질 수 있습니다. 다만 배열엔 보통 숫자 키를 쓰는 것이 규칙인데 런타임에 숫자 키는 문자열로 강제 변환됩니다.

 

이런 이유로 TypeScript의 keyof는 기본적으로 number | string | symbol 타입의 값을 반환합니다.

 

올바른 동작이지만 이 때문에 TypeScript에게 특정 키가 string이고 number나 symbol이 아니라는 사실을 증명해야 하는 귀찮은 상황에 놓일 수 있습니다.

 

TypeScript가 예전처럼 동작하길 원한다면 tsconfig.json에서 keyofStringsOnly 플래그를 활성화하면 됩니다:)

반응형