타입스크립트 심화 #1 keyof와 typeof

14 분 소요

타입스크립트 기초 강좌+ React 실전을 마쳤다면, 이제 한 단계 위 — 타입을 직접 가공하는 도구들을 다룰 차례입니다. 이 시리즈는 7편으로 구성됩니다.

  • #1 keyof와 typeof ← 이번 글
  • #2 Mapped types
  • #3 Conditional types와 infer
  • #4 Template literal types
  • #5 Discriminated union과 타입 가드 깊이
  • #6 모듈과 .d.ts
  • #7 실전 패턴과 안티패턴

기초 강좌가 "타입을 어떻게 적느냐"였다면, 이 시리즈는 "타입을 어떻게 계산하느냐" 입니다. 첫 번째 도구는 가장 기본 두 개 — keyoftypeof 입니다.

keyof — 객체 타입의 키를 union으로 모으기

keyof T 는 객체 타입 T 의 모든 키를 문자열 리터럴 union으로 만들어 줍니다.

keyof 기본
type User = {
  id: string;
  name: string;
  age: number;
};
 
type UserKey = keyof User;
// 'id' | 'name' | 'age'

처음 보면 별것 아닌 것 같지만, 이게 있어야 "T의 키 중 하나" 라는 표현이 가능해집니다. 가장 흔한 활용은 인덱스 시그니처를 안전하게 다루는 함수 예요.

안전한 getValue
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
 
const user: User = { id: 'u1', name: '커티스', age: 30 };
 
getValue(user, 'name');   // string
getValue(user, 'age');    // number
getValue(user, 'unknown'); // ✗ 'unknown'은 keyof User 가 아님

세 줄짜리 함수지만 두 가지를 동시에 보장합니다.

  1. 존재하지 않는 키는 호출 단계에서 막힘
  2. 반환 타입이 키에 따라 다르게 좁혀짐 — name 이면 string, agenumber

T[K]인덱스 액세스 타입입니다. "T라는 타입에서 키 K로 꺼낸 값의 타입"을 의미해요. 자바스크립트의 obj[key] 와 모양이 같아 직관적입니다.

인덱스 시그니처 vs 명시 키

keyof 의 결과는 객체 정의 방식에 따라 달라집니다.

명시 키 vs 인덱스 시그니처
type A = { id: string; name: string };
type AK = keyof A;        // 'id' | 'name' (좁은 union)
 
type B = { [key: string]: string };
type BK = keyof B;        // string | number  (number는 자바스크립트가 키를 자동 변환하므로)

명시적으로 적은 키만 좁은 리터럴로 잡힙니다. 인덱스 시그니처를 쓰면 keyof 의 정밀도가 떨어져요. 가능하면 객체 모양을 명시적으로 적는 게 타입 안전 관점에서 유리합니다.

숫자 키도 union에 들어옴 — 배열의 keyof

배열 타입의 keyof 는 인덱스(숫자)와 메서드 이름을 모두 포함합니다.

keyof Array — 거의 안 씀
type Arr = string[];
type ArrK = keyof Arr;
// number | 'length' | 'toString' | 'push' | ...

배열에 keyof 를 직접 쓸 일은 거의 없습니다. T[number] 로 원소 타입을 꺼내는 패턴이 더 자주 쓰여요.

원소 타입 추출
type Arr = { id: string; name: string }[];
type Item = Arr[number];   // { id: string; name: string }

number 는 "임의의 숫자 인덱스로 접근했을 때의 타입" 을 의미합니다. 튜플이 아닌 일반 배열에서 가장 짧은 원소-타입 추출 패턴이에요.

typeof — 값에서 타입을 끌어내기

타입스크립트의 typeof 는 자바스크립트의 typeof이름은 같지만 다른 자리에서 쓰입니다.

  • 자바스크립트: 값을 표현하는 자리 — typeof x === 'string' (값 단계)
  • 타입스크립트: 타입을 표현하는 자리let y: typeof x (타입 단계)
typeof 기본
const config = {
  baseUrl: 'https://api.example.com',
  timeout: 5000,
};
 
type Config = typeof config;
// { baseUrl: string; timeout: number }

이미 적어 놓은 값에서 타입을 끌어옵니다. 값을 한 번만 쓰고, 타입은 거기서 자동으로 따라오게 하는 게 핵심 효용이에요. 같은 정보를 두 군데에 적는 중복을 없애 줍니다.

함수에도, import에도

typeof 는 어떤 값에든 쓸 수 있습니다.

함수 타입 추출
function createUser(name: string, age: number) {
  return { id: crypto.randomUUID(), name, age };
}
 
type CreateUser = typeof createUser;
// (name: string, age: number) => { id: string; name: string; age: number }
 
type User = ReturnType<typeof createUser>;
// { id: string; name: string; age: number }

ReturnType<typeof fn> 패턴은 정말 자주 씁니다. "이 함수가 반환하는 객체의 타입"을 별도로 적지 않고 끌어오는 거예요. ReturnType 의 정의는 #3에서 직접 만들어 보겠습니다.

모듈 import도 typeof
import * as utils from './utils';
 
type Utils = typeof utils;   // 모듈 전체의 타입

keyof typeof — 가장 자주 쓰는 조합

keyof 는 타입을 받고 typeof 는 값에서 타입을 만듭니다. 둘을 합치면 값으로 정의된 객체의 키들 을 union으로 끌어올 수 있어요.

값에서 키 union 만들기
const STATUS = {
  idle: 'idle',
  loading: 'loading',
  done: 'done',
  error: 'error',
} as const;
 
type StatusKey = keyof typeof STATUS;
// 'idle' | 'loading' | 'done' | 'error'
 
type StatusValue = typeof STATUS[StatusKey];
// 'idle' | 'loading' | 'done' | 'error'

자바스크립트로 enum-like 객체를 정의하고, 거기서 union 타입을 자동으로 만들어내는 게 핵심입니다. 객체에 키를 추가하면 union도 자동으로 늘어나요. 단일 출처(single source of truth) 를 만드는 패턴이라 매우 가치 있습니다.

as const 가 핵심 열쇠

as const 가 없으면 같은 코드가 다음처럼 무너집니다.

as const 없이
const STATUS = {
  idle: 'idle',
  loading: 'loading',
};
 
type StatusValue = typeof STATUS[keyof typeof STATUS];
// string  ← 너무 넓다

as const 가 있어야 타입스크립트가 각 값을 리터럴 타입으로 좁힙니다. 객체나 배열을 "이 모양 그대로, 이 값 그대로 고정"하라는 신호예요. as const 가 없으면 모든 게 그냥 string, number 로 넓어져 버립니다.

as const 의 효과를 정확히 표로 보면:

as const 없을 때와 있을 때
const a = ['red', 'green', 'blue'];
//    ^ string[]
const b = ['red', 'green', 'blue'] as const;
//    ^ readonly ['red', 'green', 'blue']
 
const c = { mode: 'dark' };
//    ^ { mode: string }
const d = { mode: 'dark' } as const;
//    ^ { readonly mode: 'dark' }

as const 를 붙이면 타입은 그 값 그대로가 됩니다. 배열은 readonly 튜플로, 객체는 모든 필드가 readonly 인 리터럴로요. "내가 적은 그대로 고정" 이라고 기억하면 직관적입니다.

실전 1 — 라우트 맵에서 union 만들기

API 라우트나 페이지 라우트를 객체로 정의하고, 그 키를 안전한 union으로 쓰고 싶을 때.

route map
const ROUTES = {
  home: '/',
  about: '/about',
  blog: '/blog',
  blogPost: '/blog/:slug',
} as const;
 
type RouteName = keyof typeof ROUTES;
// 'home' | 'about' | 'blog' | 'blogPost'
 
type RoutePath = typeof ROUTES[RouteName];
// '/' | '/about' | '/blog' | '/blog/:slug'
 
function navigate(name: RouteName) {
  const path = ROUTES[name];
  // ...
}
 
navigate('home');     // OK
navigate('contact');  // ✗ contact는 정의되지 않음

ROUTES 에 새 라우트를 추가하면 RouteName 도 자동으로 확장됩니다. 한 군데서 정의 → 타입 자동 동기화.

실전 2 — 액션 type을 자동으로 모으기

reducer 액션을 객체로 정의하고, 거기서 union을 자동 생성하는 패턴.

액션 타입 자동화
const ActionTypes = {
  ADD_TODO: 'todos/add',
  REMOVE_TODO: 'todos/remove',
  TOGGLE_TODO: 'todos/toggle',
} as const;
 
type ActionType = typeof ActionTypes[keyof typeof ActionTypes];
// 'todos/add' | 'todos/remove' | 'todos/toggle'

이렇게 두면 dispatch({ type: ActionTypes.ADD_TODO, ... }) 형태로 호출할 때 타입이 정확히 맞고, 타입 정의를 별도로 유지하지 않아도 됩니다.

keyof 가 빛나는 자리 — 폼 필드 검증

폼을 객체로 다루고, 필드 이름을 타입으로 좁히고 싶을 때.

필드 이름 안전 검증
type SignupForm = {
  email: string;
  password: string;
  agree: boolean;
};
 
function setField<K extends keyof SignupForm>(
  form: SignupForm,
  key: K,
  value: SignupForm[K]
): SignupForm {
  return { ...form, [key]: value };
}
 
const form: SignupForm = { email: '', password: '', agree: false };
 
setField(form, 'email', 'me@example.com');   // OK
setField(form, 'agree', true);               // OK
setField(form, 'agree', 'yes');              // ✗ agree는 boolean
setField(form, 'unknown', '...');            // ✗ unknown 키 없음

세 줄짜리 함수에 존재하지 않는 키 차단키별 값 타입 일치 두 가지가 한꺼번에 들어가 있어요. 자바스크립트로는 문서나 if문으로 막아야 했던 영역입니다.

마무리

이번 글에서 정리한 내용:

  • keyof T — 객체 타입의 키들의 union
  • T[K] — 인덱스 액세스 타입, 키로 값 타입 꺼내기
  • T[number] — 배열의 원소 타입 추출
  • typeof value — 값에서 타입 끌어오기, 타입 단계의 표현
  • as const — 리터럴 타입을 그대로 고정 (객체 + 배열)
  • keyof typeof OBJ — 값으로 정의한 객체에서 키 union 자동 생성
  • 실전 — 라우트 맵, 액션 타입, 폼 필드 안전 검증

이 두 도구가 익숙해지면 다음 도구들의 기반이 잡힙니다. 다음 글(#2 Mapped types)에서는 keyof 위에서 한 단계 더 — 객체 타입을 통째로 변환하는 mapped types를 다룹니다. Partial, Required, Readonly 같은 빌트인 유틸리티가 어떻게 만들어졌는지도 직접 짜보면서요.