기존 타입/값에서 새 타입을 파생하는 도구들. 타입의 단일 출처(single source of truth)를 만드는 데 핵심. 상수 하나만 고치면 관련 타입이 전부 따라오게 만드는 게 목표다.
이 글에서 다루는 것:
as constkeyof typeofas const언제 쓰나: 배열이나 객체 리터럴을 넓은 타입이 아니라 정확한 리터럴 타입으로 고정하고 싶을 때. 상수 목록에서 유니온 타입을 뽑는 출발점.
문제 상황: 그냥 선언하면 타입이 넓어진다.
const categories = ["food", "beverage", "goods"];
// 타입: string[] ← "food" 같은 리터럴 정보 사라짐
패턴: as const로 읽기 전용 리터럴로 고정한다.
const categories = ["food", "beverage", "goods"] as const;
// 타입: readonly ["food", "beverage", "goods"]
type Category = typeof categories[number];
// "food" | "beverage" | "goods"
이렇게 하면 카테고리 목록이 categories 한 곳에서만 관리되고, 타입은 거기서 자동으로 파생된다. 새 카테고리를 추가할 때 배열만 고치면 타입이 따라온다.
주의점: as const가 붙은 값은 읽기 전용이 된다. push() 등 변경 메서드를 쓰려 하면 에러 — 상수니까 의도된 동작이다. 값을 좁은 타입으로 우기는 as Category와는 완전히 다른 용도이니 혼동하지 말 것. (as const는 "최대한 좁게 읽어달라"는 요청이고, as Category는 "검증 없이 이 타입이라 쳐"라는 우기기다.)
keyof typeof언제 쓰나: 객체의 키들만 허용하는 타입이 필요할 때. 라벨 맵, 설정 객체, 상수 테이블에서 키 목록을 타입으로 재사용.
문제 상황: 객체 키를 문자열로 받으면 오타를 못 잡는다.
const LABELS = { food: "식품", beverage: "음료", goods: "잡화" };
function getLabel(key: string) {
return LABELS[key]; // key가 뭐든 통과, 오타 못 잡음
}
패턴: typeof로 값의 타입을, keyof로 그 키들을 유니온으로 뽑는다.
const LABELS = { food: "식품", beverage: "음료", goods: "잡화" } as const;
type LabelKey = keyof typeof LABELS;
// "food" | "beverage" | "goods"
function getLabel(key: LabelKey) {
return LABELS[key]; // ✅ 정의된 키만 허용
}
주의점: typeof(값 → 타입)와 keyof(타입 → 키 유니온)는 방향이 다른 별개 연산자다. keyof typeof X는 "X라는 값의 타입에서 키를 뽑아라"로 읽으면 된다. as const와 짝을 이룰 때 가장 강력하다.
언제 쓰나: 이미 있는 타입을 조금 변형한 타입이 필요할 때. 매번 새로 정의하지 말고 조합한다.
| 유틸리티 | 하는 일 | 예시 상황 |
|---|---|---|
Partial<T> | 모든 속성 옵셔널 | 수정 폼(일부만 업데이트) |
Required<T> | 모든 속성 필수 | 옵셔널 제거 |
Pick<T, K> | 일부 속성만 추출 | 목록용 축약 타입 |
Omit<T, K> | 일부 속성 제외 | id 뺀 생성 요청 타입 |
Record<K, V> | 키-값 맵 타입 | 카테고리별 설정 |
ReturnType<F> | 함수 반환 타입 추출 | 함수 결과 재사용 |
type User = { id: number; name: string; email: string };
type UserUpdate = Partial<User>; // 전부 옵셔널
type UserPreview = Pick<User, "id" | "name">; // { id, name }
type NewUser = Omit<User, "id">; // id 제외
type Settings = Record<Category, boolean>; // { food: boolean, ... }
function createUser() { return { id: 1, name: "김" }; }
type Created = ReturnType<typeof createUser>; // { id: number; name: string }
주의점: 유틸리티 타입은 조합할 때 강력하다. Partial<Omit<User, "id">>처럼 중첩 가능. 다만 과하게 중첩하면 가독성이 떨어지니, 자주 쓰는 조합은 별도 타입으로 이름을 붙이는 게 낫다.
언제 쓰나: 문자열의 형식/패턴 자체를 타입으로 강제하고 싶을 때. ID 접미사, 이벤트 이름, CSS 단위 등.
문제 상황: string으로 받으면 형식이 자유로워 실수가 난다.
function getById(id: string) {} // "user_1"이든 "asdf"든 통과
패턴: 백틱과 ${}로 문자열 패턴을 타입화한다.
type UserId = `user_${number}`;
type EventName = `on${Capitalize<string>}`; // onClick, onScroll...
const id: UserId = "user_1"; // ✅
const bad: UserId = "user_x"; // ❌ number 자리에 x
주의점: 유니온과 결합하면 조합이 폭발적으로 늘어난다. `${A|B}_${C|D}`는 4가지 조합을 만든다. 편리하지만 너무 큰 유니온은 컴파일러를 느리게 만들 수 있으니 적당히.
| 상황 | 패턴 |
|---|---|
| 상수 배열/객체를 리터럴로 고정 | as const |
| 객체 키만 허용하는 타입 | keyof typeof |
| 기존 타입 변형 | Partial/Pick/Omit/Record/ReturnType |
| 문자열 형식을 타입으로 강제 | 템플릿 리터럴 타입 |
타입 조작의 관통 원칙: 타입을 손으로 중복 정의하지 말고, 값이나 기존 타입에서 파생하라. 단일 출처가 유지되면 변경에 강해진다.
이전 편: 좁히기 — 타입 가드, discriminated union, satisfies
다음 편: 실무 기본기 — 제네릭, 오버로드, unknown과 API 에러 처리