keyof T는 객체 타입 T의 속성 이름들을 모아 리터럴 유니언으로 꺼내는 문법이고, T['속성이름'](인덱스 접근 타입)은 그 속성 줄에 적힌 값의 타입을 꺼내는 문법입니다. 이미 선언한 타입에서 속성 이름과 속성 타입을 직접 뽑아 쓰면, 원래 타입이 바뀔 때 함수를 고치지 않아도 검사기가 알아서 새 기준을 적용합니다.
객체에서 속성 이름을 받아 값을 꺼내는 함수 read(m, k)를 만들 때, k를 넓은 string으로 적으면 'prise' 같은 오타도 지나가거나 TS7053 오류가 납니다. 그렇다고 'name' | 'price'를 손으로 또 적으면 나중에 메뉴 속성이 늘어날 때 두 군데를 고쳐야 합니다. 이 레슨에서는 있는 객체 타입에서 이름 모음과 속성 타입을 꺼내는 법을 살펴봅니다.
1keyof — 속성 이름의 유니언 꺼내기
Menu 앞에 keyof를 붙인 keyof Menu는 Menu가 가진 속성 이름들의 모음인 "name" | "price"와 같은 뜻입니다(리터럴 유니언으로 값 고르기). 다만 검사기가 칩이나 오류 문장을 찍을 때는 풀어 쓴 유니언 대신 keyof Menu라는 이름표를 그대로 보여 줄 때가 많습니다.
interface Menu {
name: string;
price: number;
}
type Key = keyof Menu;
function read(m: Menu, k: Key) {
return m[k];
}
앞 코드에 이어 latte에서 두 속성을 꺼내 찍어 봅니다.
const latte: Menu = {
name: '라테',
price: 4500,
};
console.log(
read(latte, 'name'),
read(latte, 'price'),
);
라테 45007번 줄의 매개변수 k는 keyof Menu, 곧 'name' 또는 'price'만 받습니다. 따라서 8번 줄 m[k]는 없는 속성을 꺼낼 걱정이 없어 검사를 지나고, 돌려주는 타입은 두 속성의 타입을 합친 string | number가 됩니다.
만약 Menu에 없는 속성 이름 'prise'를 넘기면 어떻게 될까요?
interface Menu {
name: string;
price: number;
}
type Key = keyof Menu;
type Price = Menu['price'];
function read(m: Menu, k: Key) {
return m[k];
}
const latte: Menu = {
name: '라테',
price: 4500,
};
read(latte, 'prise');
main.ts(14,13): error TS2345: Argument of type '"prise"' is not assignable to parameter of type 'keyof Menu'.14번 줄에서 "prise"는 'keyof Menu'("name" | "price") 자리에 넣을 수 없다는 TS2345 오류가 납니다.
JS 다시 보기: keyof Menu는 검사할 때만 쓰이고 지워지는 타입입니다. 실행할 때 객체의 속성 이름 배열이 필요하면 Object.keys(latte)를 씁니다(객체 돌기).
2대괄호로 속성 하나의 타입 꺼내기 — Order['kind']
객체 타입 뒤에 대괄호로 속성 이름을 적으면(Order['kind']), 그 속성 줄에 적힌 타입을 그대로 꺼냅니다. 값에서 order['kind']로 속성 값을 꺼내는 것과 겉모양은 비슷하지만, 타입 자리에 적혀 타입을 꺼낸다는 점이 다릅니다(검사 규칙 4 · 7).
interface Order {
kind: 'drink' | 'food';
qty: number;
}
type Kind = Order['kind'];
type Qty = Order['qty'];
const k: Kind = 'drink';
const q: Qty = 2;
console.log(k, q);
drink 25번 줄의 Order['kind']는 Order의 kind 줄에 적힌 "drink" | "food"를 꺼내고, 6번 줄의 Order['qty']는 number를 꺼냅니다. 나중에 Order의 kind 줄에 | 'goods'를 더하면 Kind도 저절로 "drink" | "food" | "goods"로 바뀝니다.
대괄호 안에는 속성 이름 하나뿐 아니라 keyof로 만든 유니언도 넣을 수 있습니다. 아래 6번 줄의 Val은 어떤 타입이 될까요? 답을 정한 뒤 펼쳐 보세요.
interface Menu {
name: string;
price: number;
}
type Val = Menu[keyof Menu];
const v: Val = true;
검사 결과 보기
main.ts(6,7): error TS2322: Type 'boolean' is not assignable to type 'Val'.예상과 다르면 그곳이 배울 곳입니다.
keyof Menu는 "name" | "price"이므로, Menu[keyof Menu]는 Menu['name']의 string과 Menu['price']의 number를 합친 string | number가 됩니다. 그래서 6번 줄에 boolean 값인 true를 넣으면 TS2322로 막힙니다.
| 타입 표기 | 꺼내는 것 | Menu에서의 결과 |
|---|---|---|
keyof Menu | 속성 이름들의 유니언 | "name" | "price" (칩은 keyof Menu) |
Menu['price'] | price 줄 하나의 타입 | number |
Menu[keyof Menu] | 모든 속성 값 타입의 유니언 | string | number |
예제
검사 결과와 실행 결과를 먼저 적어 본 뒤 펼쳐 보세요. 맞혀 보기는 점수에 들지 않습니다.
Order에서 kind 줄의 타입을 꺼내 매개변수 타입으로 쓴 함수 label입니다.
interface Order {
kind: 'drink' | 'food';
qty: number;
}
function label(
k: Order['kind'],
) {
return k === 'drink'
? '음료'
: '음식';
}
console.log(label('drink'));
console.log(label('dessert'));
검사를 지나갈까요? 지나가지 못하면 몇 번 줄에서 막힐까요? Node로 돌리면 무엇이 찍힐까요?
검사 결과와 실행 결과 보기
main.ts(14,19): error TS2345: Argument of type '"dessert"' is not assignable to parameter of type '"drink" | "food"'.음료
음식예상과 다르면 그곳이 배울 곳입니다.
7번 줄의 Order['kind']는 "drink" | "food"이므로, 14번 줄에서 'dessert'를 넘기면 TS2345로 막힙니다.
Node는 타입을 지우고 실행하므로(검사 규칙 1), 14번 줄의 'dessert'도 k === 'drink'가 거짓인 갈래로 가서 음식을 찍습니다.
자주 틀리는 곳
틀린 생각: keyof Menu를 값 자리에 쓰면 실행할 때 ['name', 'price'] 배열이 만들어진다.
이름을 모아 준다고 하니 자바스크립트의 Object.keys처럼 실행 값으로도 쓸 수 있을 것 같습니다(타입이 실행 때 남는다는 생각).
interface Menu {
name: string;
price: number;
}
console.log(keyof Menu);
main.ts(5,19): error TS1005: ',' expected.맞는 규칙: keyof는 오직 타입 자리에만 쓸 수 있는 문법이며 실행 전에 지워집니다(검사 규칙 1). 실행할 때 속성 이름 목록이 필요하면 Object.keys(객체)를 씁니다(1절).
틀린 생각: 오류 문장에 찍힌 'keyof Menu'는 "name" | "price"와 다른 별개의 타입이다.
편집기 칩이나 TS2345 오류 문장이 풀어 쓴 "name" | "price" 대신 'keyof Menu'라고 적으니 처음 보는 타입처럼 보입니다.
맞는 규칙: 검사기는 어디서 나온 타입인지 알아보도록 keyof Menu라는 이름을 남겨 찍을 뿐, 실제 알맹이는 "name" | "price" 리터럴 유니언과 똑같습니다(1절).
틀린 생각: 타입 자리에도 변수 이름을 넣어 latte['price']라고 적거나, 점 표기법으로 Order.kind라고 적는다.
자바스크립트에서 값을 꺼낼 때 latte['price']나 latte.price를 쓰니 타입 자리에서도 똑같이 적곤 합니다(타입과 값을 섞는 생각).
interface Order {
kind: 'drink' | 'food';
}
const o: Order = {
kind: 'drink',
};
type A = o['kind'];
main.ts(7,10): error TS2749: 'o' refers to a value, but is being used as a type here. Did you mean 'typeof o'?맞는 규칙: 속성 타입을 꺼낼 때는 값 이름(o)이 아니라 타입 이름에 대괄호를 붙여 Order['kind']로 적습니다(점 표기법 Order.kind도 쓸 수 없습니다). 값 이름에서 타입을 먼저 뽑는 typeof o는 다음 레슨 값에서 타입 뽑기 — typeof와 as const에서 배웁니다.
확인 문제
1. 다음 코드에서 15번 줄은 검사를 지나갈까요? 지나가지 못하면 어떤 오류가 날까요?
interface Book {
title: string;
pages: number;
}
function pick(
b: Book,
k: keyof Book,
) {
return b[k];
}
const b: Book = {
title: '코스모스',
pages: 500,
};
pick(b, 'author');
힌트
keyof Book이 허용하는 속성 이름 두 개가 무엇인지 보고, 15번 줄에서 넘긴 'author'가 그 안에 있는지 확인합니다.
정답
main.ts(15,9): error TS2345: Argument of type '"author"' is not assignable to parameter of type 'keyof Book'.15번 줄에서 TS2345로 막힙니다. keyof Book은 "pages" | "title"인데, 그 안에 없는 'author'를 넘겼기 때문입니다.
흔한 오답은 '9번 줄 return b[k];에서 막힌다'입니다. k가 keyof Book이므로 9번 줄은 검사를 지나고, 없는 이름을 넘긴 15번 줄이 막힙니다.
2. Score 인터페이스에서 grade 속성 줄의 타입('A' | 'B' | 'C')을 그대로 꺼내 변수 myGrade의 타입으로 적으려고 합니다. 5번 줄 빈칸을 채우세요.
interface Score {
subject: string;
grade: 'A' | 'B' | 'C';
}
const myGrade: ________ = 'A';
console.log(myGrade);
힌트
타입 이름 Score 뒤에 대괄호로 속성 이름을 적습니다(2절).
정답
빈칸은 Score['grade']입니다.
interface Score {
subject: string;
grade: 'A' | 'B' | 'C';
}
const myGrade:
Score['grade'] = 'A';
console.log(myGrade);A다르게 써도 되는 곳: 'A' | 'B' | 'C'를 직접 적어도 검사는 지나지만, 나중에 Score에 'D'가 추가될 때 함께 바뀌지 않습니다.
흔한 오답은 Score.grade입니다. 타입에서 속성 타입을 꺼낼 때는 점(.)이 아니라 대괄호(['grade'])를 써야 합니다.
3. 아래 Ticket 인터페이스에서 만들어지는 세 타입 A, B, C의 알맹이를 바르게 짝지은 것을 고르세요(유니언 차례는 상관없음).
interface Ticket {
seat: string;
price: number;
}
type A = keyof Ticket;
type B = Ticket['price'];
type C = Ticket[keyof Ticket];
- ①
A:"price" | "seat",B:number,C:string | number - ②
A:string | number,B:"price",C:"price" | "seat" - ③
A:"price" | "seat",B:string,C:number
힌트
2절 끝의 요약 표에서 keyof T, T['k'], T[keyof T] 세 가지를 비교해 보세요.
정답
①입니다. keyof Ticket은 속성 이름들의 유니언("price" | "seat"), Ticket['price']는 price 줄의 타입(number), Ticket[keyof Ticket]은 모든 속성 값 타입의 유니언(string | number)입니다.
- ②
keyof Ticket과Ticket[keyof Ticket]을 거꾸로 읽은 것입니다. - ③
Ticket['price']는seat가 아니라price줄의 타입이므로number입니다.
컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요.
1절 첫 코드를 main.ts에 저장하고 15번 줄의 'price'를 'prise'로 바꾼 뒤 npx tsc를 돌려 TS2345가 나는지 확인해 보세요. 다음 레슨은 값에서 타입 뽑기 — typeof와 as const입니다.
검사하고 돌리는 법은 내 컴퓨터에서 검사하고 돌리기에 있습니다.