`keyof` 타입 연산자

keyof 타입 연산자

TypeScript로 코드를 짜다 보면, 객체 타입의 속성 이름(키)들을 그대로 타입으로 꺼내 쓰고 싶을 때가 있어요. 가령 어떤 객체가 x, y라는 키를 가질 때, 그 키들의 조합을 타입으로 표현하고 싶은 거죠. 그럴 때 쓰는 연산자가 바로 **keyof**예요. 이번 글에서는 keyof가 정확히 어떤 일을 하는지, 그리고 인덱스 시그니처를 가진 타입에서는 또 어떻게 동작하는지 차근차근 살펴볼게요.

출처: TypeScript 공식문서

본문

keyof 연산자는 객체 타입을 받아서, 그 키들로 이루어진 문자열 또는 숫자 리터럴 유니온 타입을 만들어 줘요. 정의만 보면 와닿지 않으니, 바로 예시를 볼게요.

먼저 Point라는 타입을 하나 만든 다음, keyof를 적용해 볼게요.

type Point = { x: number; y: number };
type P = keyof Point;

Point에는 속성이 x, y 딱 두 개 있죠. 그래서 keyof Point"x" | "y"를 만들어 내요. 즉 아래 P 타입은 "x" | "y"라고 직접 적어 준 것과 완전히 같은 타입이에요.

type P = "x" | "y";

이렇게 객체 타입의 키를 하나하나 적지 않아도 keyof 한 번으로 자동으로 뽑아 주니, 타입이 바뀌어도 그대로 따라오는 게 장점이에요.

이번에는 조금 다른 경우를 볼게요. 타입에 문자열 또는 숫자 인덱스 시그니처가 있다면, keyof는 키 이름의 리터럴을 돌려주는 대신 시그니처가 가리키는 타입을 돌려줘요.

type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish;
// type A = number

type Mapish = { [k: string]: boolean };
type M = keyof Mapish;
// type M = string | number

여기서 Anumber가 되고, Mstring | number가 돼요. 그런데 M이 왜 string만이 아니라 string | number일까요? 그 이유가 꽤 흥미로운데요. JavaScript에서 객체의 키는 언제나 문자열로 강제 변환되거든요. 그래서 obj[0]은 항상 obj["0"]과 똑같이 동작해요. 숫자로 써도 결국 문자열 키로 취급되다 보니, keyof도 그 사실을 반영해서 number까지 포함해 주는 거예요.

많은 분이 이 부분을 처음에 받아들이기 어려워하는데, 실제로 코드를 쓸 때는 이 특성을 그대로 체감하게 돼요. 특히 배열 형태의 객체나 문자열 키를 섞어 쓰는 타입에서 keyof의 결과가 예상과 다르게 보이면, 대부분 이 강제 변환 때문이라고 보면 돼요.

그리고 하나 더 짚어 둘게요. keyof 타입은 단독으로 쓰일 때보다 나중에 배울 매핑된 타입(mapped types)과 함께 쓸 때 특히 강력해져요. 매핑된 타입은 객체 타입의 각 키를 순회하면서 새로운 타입을 만들어 내는 도구인데, 그때 키 목록을 얻는 데 keyof가 핵심 역할을 해요. 지금은 "그런 게 있구나" 정도만 기억해 두셔도 충분해요.

더 알아보기

  • 매핑된 타입 (Mapped Types): keyof와 짝을 이루는 기능이에요. 각 키를 순회하며 타입을 변환하는 방법을 다뤄요. → Mapped Types
  • 타입 호환성: 객체 타입끼리 어떻게 할당되는지 이해하면 keyof의 결과 타입을 활용하기 편해져요.