TypeScript를 쓰다 보면 keyof, T[K], [P in K] 같은 문법을 자주 만나게 됩니다. 처음에는 각각 따로 외워야 하는 문법처럼 보이지만, 실제로는 하나의 흐름으로 연결되어 있습니다.
핵심은 간단합니다. 객체 타입에서 키를 타입으로 꺼내고, 그 키에 해당하는 값의 타입을 다시 조회한 뒤, 필요한 형태의 새 객체 타입을 만드는 것입니다.
keyof는 객체 타입의 키를 유니온으로 만든다
먼저 keyof T는 객체 타입 T가 가진 키들을 유니온 타입으로 만들어줍니다.
type User = {
id: number;
name: string;
email: string;
isAdmin: boolean;
};
type UserKey = keyof User;
// "id" | "name" | "email" | "isAdmin"
이제 UserKey 타입에는 "id", "name", "email", "isAdmin" 중 하나만 들어갈 수 있습니다. 아무 문자열이나 허용하지 않고, 실제 User 타입에 존재하는 키만 허용하는 것이 중요합니다.
const userKey: UserKey = "email";
이렇게 하면 키 이름을 문자열로 다루더라도 TypeScript가 안전하게 검사할 수 있습니다.
T[K]는 특정 키의 값 타입을 꺼낸다
T[K]는 객체 값을 실제로 읽는 문법이 아닙니다. 타입 시스템 안에서 “이 키가 가리키는 값의 타입이 무엇인가?”를 꺼내는 문법입니다.
type UserName = User["name"];
// string
type UserPrimitiveField = User["id" | "name" | "email" | "isAdmin"];
// number | string | boolean
User["name"]은 User 타입에서 name 필드의 타입인 string을 꺼냅니다. 여러 키를 넣으면 각 키의 값 타입을 모아 유니온으로 만듭니다.
이 문법을 잘 이해하면 타입을 손으로 다시 쓰지 않고, 기존 타입에서 필요한 정보만 정확히 가져올 수 있습니다.
K extends keyof T가 필요한 이유
제네릭으로 타입을 만들 때는 K가 아무 키나 될 수 없도록 제한해야 합니다.
function getValue<T, K extends keyof T>(object: T, key: K): T[K] {
return object[key];
}
여기서 K extends keyof T는 K가 반드시 T의 실제 키 중 하나여야 한다는 뜻입니다. 이 제약이 있어야 T[K]를 안전하게 사용할 수 있습니다.
const userEmail = getValue(user, "email");
// string
예를 들어 User 객체에서 "email"을 꺼내면 TypeScript는 반환 타입을 string으로 추론합니다. 반대로 존재하지 않는 키를 넣으면 컴파일 단계에서 막아줍니다.
매핑 타입은 키를 순회해 새 객체 타입을 만든다
매핑 타입은 키 유니온을 하나씩 순회하면서 새로운 객체 타입을 만듭니다.
type PickFields<T, K extends keyof T> = {
[P in K]: T[P];
};
이 코드는 TypeScript 내장 유틸리티 타입인 Pick<T, K>의 핵심 구조와 거의 같습니다.
K extends keyof T: 고를 수 있는 키를 원본 타입의 키로 제한[P in K]: 선택된 키들을 하나씩 순회T[P]: 원본 타입에서 해당 키의 값 타입을 가져옴
즉, PickFields<User, "id" | "name">은 User 타입에서 id와 name만 골라 새 타입을 만듭니다.
type UserSummary = PickFields<User, "id" | "name">;
const summary: UserSummary = {
id: 1,
name: "Kim",
};
실무에서는 파생 타입을 만드는 데 유용하다
실무에서는 API 응답, DTO, 폼 값, 컴포넌트 props처럼 객체 타입의 일부만 사용해야 하는 일이 많습니다. 이때 비슷한 타입을 매번 손으로 다시 작성하면 원본 타입이 바뀌었을 때 문제가 생깁니다.
예를 들어 사용자 정보 중 수정 가능한 필드만 따로 payload 타입으로 만들고 싶다면 다음처럼 작성할 수 있습니다.
type EditableUserProfile =
Partial<PickFields<User, "name" | "email" | "profile">>;
이 타입은 User에서 name, email, profile만 고른 뒤, 각 필드를 선택 입력으로 만듭니다. 원본 User 타입과 연결되어 있기 때문에 필드 타입이 바뀌어도 파생 타입이 함께 따라갑니다.
너무 복잡한 타입은 오히려 읽기 어렵다
keyof, T[K], 매핑 타입은 강력하지만 과하게 중첩하면 읽기 어려운 타입이 됩니다. 타입 안정성만큼 중요한 것은 코드 리뷰에서 의도가 바로 드러나는 것입니다.
- 원본 타입에서 파생 가능한 타입은
Pick,Omit,Partial등을 활용한다. - 반복되는 타입 선언은 유틸리티 타입으로 줄인다.
- 너무 긴 타입 조합은 별도 타입 이름을 붙여 의도를 드러낸다.
- 읽는 사람이 이해하기 어려울 정도로 중첩하지 않는다.
정리
keyof, T[K], 매핑 타입은 따로 외우는 문법이 아니라 하나의 흐름으로 이해하는 것이 좋습니다.
keyof는 객체 타입의 키를 타입으로 꺼내고, T[K]는 해당 키의 값 타입을 조회합니다. 매핑 타입은 이 둘을 조합해 새로운 객체 타입을 만듭니다.
이 흐름을 이해하면 TypeScript에서 중복 타입을 줄이고, 원본 타입과 연결된 안전한 파생 타입을 만들 수 있습니다. 특히 폼 데이터, API payload, 컴포넌트 props처럼 객체 타입을 자주 다루는 프론트엔드 코드에서 큰 도움이 됩니다.
'TS' 카테고리의 다른 글
| TypeScript Utility Types 실무 기준: Partial, Pick, Omit, Record 제대로 쓰기 (0) | 2026.07.14 |
|---|---|
| TypeScript assertion function 쉽게 이해하기 (0) | 2026.07.11 |
| TypeScript 타입 가드: boolean과 type predicate 차이 (0) | 2026.07.10 |
| TypeScript never로 분기 누락 잡기: exhaustive checking (0) | 2026.07.09 |
