TypeScript 타입 패턴 (2) — 타입 조작(Utility)

park·2026년 7월 16일

기존 타입/값에서 새 타입을 파생하는 도구들. 타입의 단일 출처(single source of truth)를 만드는 데 핵심. 상수 하나만 고치면 관련 타입이 전부 따라오게 만드는 게 목표다.

이 글에서 다루는 것:

  1. as const
  2. keyof typeof
  3. 내장 유틸리티 타입
  4. 템플릿 리터럴 타입

1. as 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는 "검증 없이 이 타입이라 쳐"라는 우기기다.)


2. 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와 짝을 이룰 때 가장 강력하다.


3. 내장 유틸리티 타입

언제 쓰나: 이미 있는 타입을 조금 변형한 타입이 필요할 때. 매번 새로 정의하지 말고 조합한다.

유틸리티하는 일예시 상황
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">>처럼 중첩 가능. 다만 과하게 중첩하면 가독성이 떨어지니, 자주 쓰는 조합은 별도 타입으로 이름을 붙이는 게 낫다.


4. 템플릿 리터럴 타입

언제 쓰나: 문자열의 형식/패턴 자체를 타입으로 강제하고 싶을 때. 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 에러 처리

profile
park

0개의 댓글