본문으로 건너뛰기

keyof와 속성 타입 꺼내기

타입스크립트 목차타입으로 타입 만들기레슨 37 / 51

목차

keyof T는 객체 타입 T의 속성 이름들을 모아 리터럴 유니언으로 꺼내는 문법이고, T['속성이름'](인덱스 접근 타입)은 그 속성 줄에 적힌 값의 타입을 꺼내는 문법입니다. 이미 선언한 타입에서 속성 이름과 속성 타입을 직접 뽑아 쓰면, 원래 타입이 바뀔 때 함수를 고치지 않아도 검사기가 알아서 새 기준을 적용합니다.

객체에서 속성 이름을 받아 값을 꺼내는 함수 read(m, k)를 만들 때, k를 넓은 string으로 적으면 'prise' 같은 오타도 지나가거나 TS7053 오류가 납니다. 그렇다고 'name' | 'price'를 손으로 또 적으면 나중에 메뉴 속성이 늘어날 때 두 군데를 고쳐야 합니다. 이 레슨에서는 있는 객체 타입에서 이름 모음과 속성 타입을 꺼내는 법을 살펴봅니다.

5단계 중 1단계. 1~5번 줄: keyof Menu는 Menu의 속성 이름만 뽑아 "name" | "price" 유니언을 만듭니다(칩은 keyof Menu로 표시).

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'),
);
실행 결과
라테 4500

7번 줄의 매개변수 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');
검사 결과 오류 1개
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 2

5번 줄의 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;
검사 결과 보기
검사 결과 오류 1개
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로 돌리면 무엇이 찍힐까요?

검사 결과와 실행 결과 보기
검사 결과 오류 1개
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);
검사 결과 오류 1개
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'];
검사 결과 오류 1개
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'가 그 안에 있는지 확인합니다.

정답
검사 결과 오류 1개
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입니다. 검사하고 돌리는 법은 내 컴퓨터에서 검사하고 돌리기에 있습니다.

끝까지 내려오면 저절로 표시됩니다. 이 기기에만 저장합니다.

타입스크립트 목차로