Menu

TypeScript keyof: 객체의 키를 타입으로 얻기

keyof는 객체 타입을 받아 속성 이름들의 유니언을 줍니다. 인터페이스와 keyof, 평범한 객체를 위한 keyof typeof, 제네릭으로 타입이 있는 속성 접근, 인덱스 시그니처(string | number), 그리고 Object.keys가 string[]를 반환하는 이유를 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

keyof는 객체 타입을 받아서 속성 이름들의 유니언을 만듭니다. 아래의 keyof User는 "id" | "name" | "email" 타입이므로, 그 타입의 변수는 이 세 문자열 중 하나만 담을 수 있습니다.

keyof는 타입 시스템 안에만 존재합니다. 코드가 컴파일될 때 지워지므로 런타임에는 아무것도 검사하지 않습니다. 대신 키가 실제 속성을 가리킨다는 컴파일 시점의 보장을 줍니다. 인터페이스에서 email의 이름을 바꾸면, keyof User로 타입이 지정된 모든 "email"이 오류가 됩니다.

keyof typeof: 평범한 객체의 키

keyof는 값이 아니라 타입에 동작합니다. 값으로 작성한 객체라면 먼저 typeof로 타입을 얻은 뒤 키를 가져오세요.

안쪽부터 읽으세요. typeof colors는 { red: string; green: string; blue: string }이고, 그것의 keyof가 키 유니언입니다. typeof 없이 keyof colors라고 쓰면 error TS2749: 'colors' refers to a value, but is being used as a type here. Did you mean 'typeof colors'?로 실패합니다.

조회 테이블에서 유니언을 끌어내는 흔한 방법입니다. 객체에 색상을 추가하면 Color 타입도 함께 커집니다. typeof에는 런타임 의미도 있으며, typeof 페이지에서 둘 다 다룹니다.

제네릭으로 타입이 있는 속성 접근

keyof의 가장 흔한 용도는 제네릭 제약입니다. K extends keyof T는 "K는 T의 키 중 하나"라는 뜻이고, T[K]는 그 키에 있는 속성의 타입입니다.

두 번째 타입 매개변수가 중요합니다. K 없이 key: keyof T라고 쓰면 반환 타입은 모든 속성 타입의 유니언인 T[keyof T](여기서는 string | number | boolean)이므로, title.toUpperCase()는 error TS2339: Property 'toUpperCase' does not exist on type 'string | number | boolean'로 실패합니다. 정확한 키를 K에 담아야 정확한 속성 타입이 유지됩니다. T[K]는 인덱스 접근 타입이며, 인덱스 접근 타입 페이지에서 설명합니다.

평범한 string으로 인덱싱하기

다른 곳에서 온 string으로 객체를 인덱싱할 때 아주 흔한 오류가 나타납니다.

컴파일러는 이렇게 출력합니다.

index.ts(10,10): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Settings'.
  No index signature with a parameter of type 'string' was found on type 'Settings'.

"colour"를 포함해 어떤 문자열이든 read에 도달할 수 있는데, Settings는 그 키에 대해 아무것도 말하지 않습니다. 해결 방법은 두 가지입니다. 실제 키만 받거나, 알 수 없는 문자열을 쓰기 전에 확인하는 것입니다.

name is keyof Settings는 타입 서술어입니다. 함수가 true를 반환하면 TypeScript는 if 안에서 input을 키로 취급합니다. 서술어는 타입 가드 페이지에서 자세히 설명합니다.

인덱스 시그니처와 keyof: string | number

인덱스 시그니처가 있는 타입이라면 keyof는 인덱스 타입을 반환하며, 문자열 인덱스 시그니처는 string만이 아니라 string | number를 줍니다.

JavaScript는 모든 숫자 속성 키를 문자열로 변환하므로 scores[42]와 scores["42"]는 같은 속성입니다. TypeScript는 number를 포함해서 이를 반영합니다. 출력은 JavaScript의 규칙 하나도 보여 줍니다. 정수 같은 키는 다른 문자열 키보다 먼저, 오름차순으로 나열됩니다. 제네릭 함수에 문자열 키만 필요하다면 Extract<keyof T, string>이나 string & keyof T라고 쓰세요. Exclude와 Extract는 별도 페이지가 있습니다.

keyof 결과 한눈에 보기

타입keyof 결과
{ id: number; name: string }"id" | "name"
{ [key: string]: number }string | number
{ [index: number]: string }number
{}never
anystring | number | symbol
{ a: 1; b: 2 } | { a: 3; c: 4 }"a"(모든 멤버가 가진 키)
{ a: 1 } & { c: 4 }"a" | "c"(어느 쪽이든 가진 키)
string[]number와 모든 배열 메서드 이름("length", "push", ...)

유니언과 교차 행은 처음에 거꾸로 보입니다. 유니언 타입의 값은 어느 멤버든 될 수 있으므로 양쪽에 모두 있는 키만 안전합니다. 교차 타입의 값은 양쪽의 모든 것을 가지므로 모든 키를 갖습니다.

Object.keys가 string[]를 반환하는 이유

Object.keys(obj)의 타입은 (keyof T)[]가 아니라 항상 string[]입니다. 이유는 구조적 타이핑입니다. 값은 선언된 타입보다 많은 속성을 가질 수 있습니다.

p의 타입은 Point지만 런타임에는 z 키가 있습니다. Object.keys가 ("x" | "y")[]를 약속한다면 그것에 기대는 코드는 p에 대해 틀릴 것입니다. 직접 만든 객체 리터럴에는 캐스팅이 안전하지만, 외부에서 온 객체라면 키를 string으로 두고 확인하세요.

매핑된 타입의 keyof

keyof는 타입의 키를 순회해 새 타입을 만드는 방법이기도 합니다. 내장 Partial과 Readonly 유틸리티 타입이 이렇게 작성되어 있습니다.

type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };

interface User {
  id: number;
  name: string;
}

type UserDraft = MyPartial<User>; // { id?: number; name?: string }

[K in keyof T]는 각 키를 방문하고, T[K]는 그 키의 속성 타입을 읽습니다. 매핑된 타입은 제한자와 키 재매핑을 다루는 별도 페이지가 있습니다.

자주 묻는 질문

TypeScript에서 keyof는 무엇을 하나요?

keyof T는 타입 T의 속성 이름들의 유니언을 만듭니다. interface User { id: number; name: string }이라면 keyof User는 "id" | "name"입니다. 컴파일 시점에 타입에만 동작하며 JavaScript를 만들지 않습니다.

TypeScript에서 keyof typeof는 무엇인가요?

keyof에는 타입이 필요한데, const colors = { red: "#f00" } 같은 평범한 객체는 값입니다. typeof colors가 값을 타입으로 바꾸고, 그다음 keyof typeof colors가 키들의 유니언, 여기서는 "red"를 줍니다. keyof colors라고 쓰면 오류 TS2749로 실패합니다.

keyof가 string | number를 반환하는 이유는 무엇인가요?

타입에 { [key: string]: number } 같은 문자열 인덱스 시그니처가 있을 때입니다. JavaScript는 숫자 속성 키를 문자열로 변환하므로 obj[42]도 올바른 접근이고, TypeScript는 키 타입에 number를 포함합니다. 문자열 키만 원한다면 Extract<keyof T, string>이나 string & keyof T를 쓰세요.

Object.keys는 왜 (keyof T)[] 대신 string[]을 반환하나요?

객체가 타입에 나열된 것보다 많은 속성을 가질 수 있기 때문입니다. 추가 필드가 있는 값을 더 좁은 타입에 대입할 수 있습니다. 결과를 (keyof T)[]로 지정하면 그런 객체에 대해 거짓말이 됩니다. 객체가 선언된 키만 가진다는 것을 안다면 캐스팅하세요: Object.keys(obj) as (keyof typeof obj)[].

"Element implicitly has an 'any' type because expression of type 'string' can't be used to index type"는 어떻게 고치나요?

인덱스가 평범한 string이고 객체 타입에 문자열 인덱스 시그니처가 없는 경우입니다. 매개변수를 string 대신 keyof YourType으로 지정하거나, 먼저 문자열을 keyof YourType으로 좁히는 타입 가드로 확인하거나, 객체가 정말로 아무 키나 받는다면 인덱스 시그니처나 Record<string, V> 타입을 주세요.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기