숏컷 코드
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: "u1", name: "Mina" };
const name = getValue(user, "name");제약과 keyof
이 카드에서 자주 보는 기본형은 아래 세 가지입니다.
- 최소 속성 제약:
T extends { length: number } - 유효한 키 집합:
keyof T - 키에 따른 값 타입:
T[K] - setter 관계 보존:
value: T[K] - 여러 제약 결합:
T extends { id: string } & { name: string }
1) 제네릭 내부에서 특정 속성이 필요하면 제약
제네릭은 기본적으로 아무 타입이나 오므로, 내부에서 .length 같은 속성을 쓰려면 extends로 최소 조건을 걸어야 합니다.
function logLength<T extends { length: number }>(value: T) {
console.log(value.length);
}제약은 타입을 좁히는 장치라기보다, 안전하게 쓸 공통 능력을 선언하는 장치입니다.
2) 객체의 유효한 키 집합은 keyof
keyof T는 객체 T에서 허용 가능한 프로퍼티 이름들을 union 타입으로 만듭니다.
interface User {
id: string;
name: string;
}
type UserKey = keyof User; // "id" | "name"즉 문자열 아무거나 받는 API를 더 정확하게 만들 수 있습니다.
3) T[K]를 쓰면 키에 따라 값 타입도 같이 따라온다
K extends keyof T로 키를 제한하고, 반환 타입을 T[K]로 두면 어떤 키를 넣었는지에 따라 값 타입이 보존됩니다.
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}이 패턴은 안전한 getter, 설정 조회, 폼 필드 접근 API에서 매우 자주 나옵니다.
const user = { id: "u1", name: "Mina" };
const id = getValue(user, "id"); // string
const name = getValue(user, "name"); // string같은 패턴으로 setter를 만들면 "키와 값이 맞는 조합만" 받게 할 수 있습니다.
function setValue<T, K extends keyof T>(obj: T, key: K, value: T[K]) {
obj[key] = value;
}4) 인덱스 시그니처가 있으면 keyof 결과가 더 넓어진다
배열이나 Record<string, ...>처럼 인덱스 시그니처가 있는 타입은 keyof 결과가 단순 리터럴 union이 아닐 수 있습니다.
type Scores = Record<string, number>;
type ScoreKey = keyof Scores; // string
type ArrayKey = keyof string[]; // number | "length" | ...즉 keyof는 "정확한 키 목록"일 수도 있고 "접근 가능한 키 축"일 수도 있어서, 대상 타입 성격을 같이 봐야 합니다.
5) 키를 그냥 string으로 받으면 타입 관계가 끊긴다
유효하지 않은 키까지 허용하게 되면, TypeScript가 보호해 줄 수 있는 범위가 크게 줄어듭니다.
그래서 객체 접근 API를 만들 때는 keyof를 먼저 떠올리는 편이 좋습니다.
Object.keys(obj)가 string[]로 나오는 것도 같은 이유라서, 그 결과를 다시 keyof typeof obj 맥락으로 연결할 helper가 필요할 때가 있습니다.
6) 제약은 "너무 넓지도 너무 좁지도 않게" 잡는다
필요 이상의 구체 제약을 걸면 재사용성이 떨어지고, 너무 넓으면 내부에서 쓸 수 있는 정보가 부족해집니다. 즉 제약은 최소 능력만 선언하는 것이 핵심입니다.
function printName<T extends { name: string }>(value: T) {
console.log(value.name);
}User 전체 타입을 강제하는 것보다 실제로 필요한 name만 제약으로 적는 편이 재사용성이 높습니다.
언제 제약을 붙일까
체크포인트
- 제네릭 내부에서 특정 속성이 필요하다:
T extends ... - 객체의 유효한 키 집합이 필요하다:
keyof T - 키에 따라 값 타입을 보존한다:
T[K] - 키와 값의 짝을 함께 강제한다:
key: K,value: T[K] - 문자열 아무거나 키로 받는다:
keyof로 바꿀지 검토 - 인덱스 시그니처 기반 타입이다:
keyof결과가 넓어지는지 먼저 확인 - 안전한 객체 접근 API다:
K extends keyof T
주의할 점
키를 그냥 string으로 받으면 존재하지 않는 프로퍼티 접근을 막기 어렵습니다.
// ❌ 임의 문자열 키
function get(obj: Record<string, unknown>, key: string) {
return obj[key];
}
// ✅ 유효한 키만 허용
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// ❌ 제약 없이 length 접근
function logLength<T>(value: T) {
// console.log(value.length);
}
// ✅ 필요한 능력만 제약으로 명시
function logSafeLength<T extends { length: number }>(value: T) {
console.log(value.length);
}참고 링크
2 sources