"keyof는 객체의 열쇠 꾸러미를 값이 아니라 타입으로 건네줘."
객체 모양에서 이름만 꺼내기
keyof T는 T가 가질 수 있는 프로퍼티 이름을 문자열 리터럴의 합집합으로 만든다. type User = { id: number, name: string }라면 keyof User는 'id' | 'name'이야. 객체를 한 번 정의한 뒤 같은 키 목록을 다른 곳에 손으로 옮겨 적지 않아도 되는 셈이지.
가장 흔한 쓰임은 안전한 프로퍼티 접근이야. function get<T, K extends keyof T>(obj: T, key: K): T[K]처럼 쓰면 호출자는 실제로 존재하는 키만 넘길 수 있어. 반환 타입도 선택한 키에 맞춰 따라오니 get(user, 'name')은 string으로 추론되고, 오타인 'nmae'는 실행 전에 막혀.
배열에서는 무엇이 키일까
keyof string[]에는 숫자 인덱스만 들어 있지 않아. length와 push 같은 배열 멤버도 모두 키이므로 결과가 생각보다 넓어져. 배열의 원소 타입을 얻고 싶은데 keyof를 쓰면 엉뚱한 문을 연 셈이야. 그때는 string[][number]처럼 숫자 인덱스 접근을 써야 해.
튜플도 같은 원리지만 각 위치 정보가 더 구체적이야. readonly [string, number]의 키에는 두 자리와 공통 배열 멤버가 함께 들어간다. 그래서 라이브러리 타입을 읽을 때는 '키의 집합이 필요한가, 원소 타입이 필요한가'를 먼저 구분해야 해.
키와 값의 관계 잇기
T[keyof T]는 모든 프로퍼티 값 타입의 합집합이야. type Config = { port: number, secure: boolean }라면 결과는 number | boolean이지. keyof가 이름의 범위를 만들고, 인덱스 접근이 그 이름에 대응하는 값을 꺼내는 역할을 나눠 맡아.
반대 방향으로는 Object.keys()가 있어. 이것은 런타임 문자열 배열을 반환하므로 타입 수준의 keyof와 똑같지 않아. 정적 타입이 실제 데이터 검증을 대신한다고 착각하면 안 돼. 외부 JSON은 여전히 런타임에서 확인해야 하지.
keyof는 객체를 복제하지 않고 그 객체가 허용하는 이름의 경계를 재사용하는 도구야. 키 목록을 따로 관리하기 시작했다면 먼저 keyof로 한 선언에서 파생할 수 있는지 봐.