본문으로 건너뛰기

키가 정해지지 않은 객체 — 인덱스 시그니처

타입스크립트 목차객체의 모양레슨 17 / 51

목차

인덱스 시그니처(index signature)는 [item: string]: number⁠처럼 적어 '어떤 이름의 칸이든 값은 number'인 모양을 나타내는 한 줄입니다. 메뉴별 판매 수나 메뉴 표처럼 칸 이름이 미리 정해지지 않은 객체를 이 줄로 받으면, 빈 객체에 칸을 더하고 대괄호로 꺼내는 자바스크립트 코드가 그대로 검사를 지나갑니다.

지금까지의 interface는 칸 이름을 줄마다 적었습니다. 그런데 빈 객체에서 시작해 주문이 들어올 때마다 메뉴 이름으로 칸을 만드는 세기 코드는, 칸 이름이 실행해 봐야 정해지니 줄로 미리 적어 둘 수 없습니다. 이런 코드를 그대로 검사하면 TS7053이 납니다. 이 레슨은 오류를 덮지 않고, 칸의 모양을 검사기에게 알려 고치는 길을 봅니다.

10단계 중 1단계. 자바스크립트 과목의 세기 코드를 그대로 검사합니다.

1자바스크립트 세기 코드를 그대로 검사하면

JS 다시 보기: 세는 코드는 빈 객체 {}에서 시작해, 주문을 하나 꺼낼 때마다 그 메뉴 칸을 1 올립니다(JS '세기: 메뉴별로 모으기'). 자바스크립트로 쓴 세기 코드를 고치지 않고 검사하고 돌려 봅니다.

const today = [
  '커피',
  '라테',
  '커피',
  '쿠키',
];
const sold = {};
for (const item of today) {
  const n = sold[item] ?? 0;
  sold[item] = n + 1;
}
console.log(sold);

검사를 지나갈까요? 지나가지 못하면 몇 번 줄에서 막힐까요? 답을 정한 뒤 펼쳐 보세요.

검사 결과 보기
검사 결과 오류 2개
main.ts(9,13): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'.  No index signature with a parameter of type 'string' was found on type '{}'.main.ts(10,3): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'.  No index signature with a parameter of type 'string' was found on type '{}'.

예상과 다르면 그곳이 배울 곳입니다. 검사는 대괄호로 칸을 꺼내는 9번 줄과 10번 줄을 짚습니다. 문장의 because 뒤가 까닭입니다. can't be used to index⁠는 'string 타입 값으로 {} 타입을 대괄호로 꺼낼 수 없다', 앞의 implicitly has an 'any' type⁠은 '그래서 꺼낸 칸의 타입이 정해지지 않아 any가 됐다'는 뜻입니다. 들여 쓴 줄 No index signature는 '{}에는 string 이름으로 꺼내는 인덱스 시그니처가 없다'입니다. 곧 이 객체에 그 이름의 칸이 있는지, 있다면 무슨 타입인지 검사기는 모른다는 뜻입니다. 7번 줄 const sold = {};의 타입은 줄이 하나도 없는 모양 {}입니다. 검사기는 나중에 실행하며 생길 칸을 미리 알 수 없습니다.

Node는 검사와 따로 돌아 자바스크립트 때와 똑같이 셉니다.

실행 결과
{ '커피': 2, '라테': 1, '쿠키': 1 }

2인덱스 시그니처 — 이름이 정해지지 않은 칸

칸 이름은 몰라도 '이름은 글자, 값은 수'라는 것은 압니다. 이것을 interface 안에 한 줄로 적습니다.

interface Sold {
  [item: string]: number;
}
const today = [
  '커피',
  '라테',
  '커피',
  '쿠키',
];
const sold: Sold = {};
for (const item of today) {
  const n = sold[item] ?? 0;
  sold[item] = n + 1;
}
console.log(sold);
실행 결과
{ '커피': 2, '라테': 1, '쿠키': 1 }

2번 줄의 대괄호 안 item: string은 '칸 이름은 string 아무것이나', 콜론 뒤 number는 '그 칸의 값은 number'입니다. 대괄호 안의 item은 이 줄에서만 쓰는 이름표라 key로 써도 같습니다. 10번 줄에서 빈 객체를 Sold 자리에 넣었습니다. 이제 검사기는 sold[item]을 '[아무 이름]: number' 줄에 맞춰 number로 봅니다. 자바스크립트 코드는 한 글자도 바꾸지 않고 타입만 붙였습니다.

칸 이름은 열어 두어도 값의 타입은 검사합니다. number 칸에 글자를 넣으면 짚습니다.

interface Sold {
  [item: string]: number;
}

const sold: Sold = {};
sold['라테'] = '두 잔';
검사 결과 오류 1개
main.ts(6,1): error TS2322: Type 'string' is not assignable to type 'number'.

남의 코드에서 Record<string, number>⁠라는 꼴을 만나면 이 Sold와 같은 뜻입니다. 8단원 'Record — 키 타입으로 만드는 객체 타입'에서 다룹니다.

3메뉴 표 찾기와 Object.keys 돌기

JS 다시 보기: Object.keys(menu)는 객체의 칸 이름을 글자 배열로 돌려줍니다(JS '객체 돌기'). 칸이 있는 메뉴 표도 대괄호로 꺼내는 이름이 string이면 같은 오류가 납니다.

const menu = {
  커피: 4000,
  라테: 4500,
};
const names = Object.keys(menu);
for (const item of names) {
  console.log(item, menu[item]);
}
검사 결과 오류 1개
main.ts(7,21): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ 커피: number; 라테: number; }'.  No index signature with a parameter of type 'string' was found on type '{ 커피: number; 라테: number; }'.

menu에는 커피와 라테 줄이 있습니다. 그러나 item은 string이라 어떤 글자든 될 수 있고, 검사기는 그 글자가 커피나 라테라고 약속할 수 없습니다. 그래서 칸이 있는 객체도 string으로 꺼내면 TS7053입니다. 메뉴 표에도 인덱스 시그니처를 붙입니다.

interface Prices {
  [item: string]: number;
}

const menu: Prices = {
  커피: 4000,
  라테: 4500,
};
const names = Object.keys(menu);
for (const item of names) {
  console.log(item, menu[item]);
}
실행 결과
커피 4000
라테 4500

세기 · 메뉴 표 찾기 · Object.keys 돌기, 자바스크립트에서 흔히 쓰는 세 꼴이 모두 같은 한 줄로 검사를 지나갑니다.

예제

빈 메뉴 표에 칸 더하기

빈 메뉴 표를 Prices로 받아 칸을 하나씩 더합니다. 인덱스 시그니처가 있으면 점으로도 칸을 더하고 꺼낼 수 있습니다.

interface Prices {
  [item: string]: number;
}

const menu: Prices = {};
menu['커피'] = 4000;
menu.라테 = 4500;
console.log(menu);
console.log(menu.라테);
실행 결과
{ '커피': 4000, '라테': 4500 }
4500

6번 줄은 대괄호로, 7번 줄은 점으로 칸을 더했습니다. 두 줄 모두 '[아무 이름]: number' 줄에 맞춰 검사합니다.

자주 틀리는 곳

틀린 생각: 인덱스 시그니처로 꺼낸 값은 늘 들어 있다.

검사기가 sold['빙수']를 짚지 않으니, 빙수 칸도 있는 것처럼 보입니다.

interface Sold {
  [item: string]: number;
}

const sold: Sold = {
  커피: 2,
  라테: 1,
};
const n = sold['빙수'];
console.log(n + 1);
실행 결과
NaN

9번 줄에서 n의 지금 타입은 number라 검사를 지납니다. 그러나 빙수 칸은 없어 n은 undefined이고, undefined + 1이 NaN을 찍습니다. 세기 코드가 ?? 0을 쓰는 까닭이 이것입니다.

interface Sold {
  [item: string]: number;
}

const sold: Sold = {
  커피: 2,
  라테: 1,
};
const n = sold['빙수'] ?? 0;
console.log(n + 1);
실행 결과
1

맞는 규칙: 인덱스 시그니처는 '칸이 있다면 그 값은 number'라는 약속일 뿐, 그 칸이 있다고 약속하지 않습니다. 없을 수 있는 칸은 ?? 0처럼 없을 때의 값을 정해 꺼냅니다(검사 규칙 2).

틀린 생각: TS7053은 검색에서 찾은 any로 덮으면 된다.

TS7053을 검색하면 const sold: any = {};⁠처럼 any를 붙이는 답이 먼저 나옵니다. 오류 문장은 사라지지만, any는 그 이름의 검사를 꺼 버려 number 칸에 글자를 넣는 실수도 함께 지나갑니다.

맞는 규칙: TS7053은 '이 객체에 그 이름의 칸이 있는지, 무슨 타입인지 모른다'는 뜻이라, 칸의 모양을 인덱스 시그니처로 적어 알려 줍니다. any로 덮지 않는 까닭은 6단원 'any는 검사를 끈다'에서 자세히 봅니다.

틀린 생각: 칸 이름이 정해진 객체도 인덱스 시그니처로 적는 편이 편하다.

인덱스 시그니처는 어떤 이름이든 받아 주니, 줄을 하나하나 적는 것보다 손이 덜 갑니다. 그 대신 이름의 오타를 잡지 못합니다.

interface Menu {
  [key: string]: string;
}

const latte: Menu = {
  name: '라테',
  size: 'tall',
};
console.log(latte.nam);
실행 결과
undefined

name을 nam으로 잘못 썼는데 검사를 지나 undefined가 찍혔습니다. Menu를 name: string; · size: string; 두 줄로 적었다면 10번 줄에서 TS2551 'nam이 없다 — 혹시 name인가요?'가 났을 것입니다.

맞는 규칙: 인덱스 시그니처는 어떤 이름이든 받아 주므로, 칸 이름이 정해진 객체에 쓰면 이름의 오타를 잡지 못합니다. 칸 이름을 미리 알면 interface에 줄로 적고, 실행해 봐야 정해지는 칸에만 인덱스 시그니처를 씁니다.

확인 문제

1. 2절 코드의 13번 줄을 sold[item] = `${n + 1}잔`;⁠으로 바꿨습니다. 검사를 지나갈까요? 막힌다면 몇 번 줄일까요?

interface Sold {
  [item: string]: number;
}
const today = [
  '커피',
  '라테',
  '커피',
  '쿠키',
];
const sold: Sold = {};
for (const item of today) {
  const n = sold[item] ?? 0;
  sold[item] = `${n + 1}잔`;
}
console.log(sold);
힌트

2번 줄이 칸 이름과 함께 무엇을 정해 두었는지 봅니다. 13번 줄 오른쪽 값의 타입을 따져 봅니다.

정답

13번 줄에서 막힙니다.

검사 결과 오류 1개
main.ts(13,3): error TS2322: Type 'string' is not assignable to type 'number'.

인덱스 시그니처는 칸 이름만 열어 두고 값은 number로 정했습니다. 템플릿 문자열은 글자라 number 칸에 넣을 수 없습니다.

흔한 오답은 '지나간다'입니다. 인덱스 시그니처를 적으면 칸에 무엇이든 넣을 수 있다고 본 것입니다. 무엇이든 되는 것은 칸 이름 쪽입니다.

2. 가게의 재고 수를 세는 코드입니다. 빈칸에 한 줄을 써서 검사를 지나게 하세요. 다 채운 코드는 { '우유': 3, '빵': 5 }를 찍어야 합니다.

interface Stock {
  ________________
}

const stock: Stock = {};
stock['우유'] = 3;
stock['빵'] = 5;
console.log(stock);
힌트

칸 이름(우유 · 빵)은 미리 정해 두지 않고, 칸의 값은 모두 수입니다.

정답

[item: string]: number;⁠입니다.

interface Stock {
  [item: string]: number;
}

const stock: Stock = {};
stock['우유'] = 3;
stock['빵'] = 5;
console.log(stock);
실행 결과
{ '우유': 3, '빵': 5 }

다르게 써도 되는 곳: 대괄호 안 이름표는 [key: string]이나 [name: string]이어도 같습니다.

흔한 오답은 item: number;입니다. 이것은 이름이 item인 칸 하나를 꼭 가진 모양이라, 빈 객체를 넣는 5번 줄은 TS2741(item이 빠짐), 6 · 7번 줄은 TS7053(우유 · 빵 칸이 없음)으로 막힙니다.

3. 수학 점수를 모은 코드입니다. 검사를 지나갈까요? Node로 돌리면 무엇이 찍힐까요? 서연의 점수가 없을 때 0점으로 셈하려면 어느 줄을 어떻게 고칠까요?

interface Scores {
  [name: string]: number;
}

const math: Scores = {
  민지: 90,
  도윤: 85,
};
const a = math['민지'];
const b = math['서연'];
console.log(a + b);
힌트

10번 줄에서 b의 지금 타입과, 실행할 때 b에 실제로 붙는 값을 따로 따져 봅니다.

정답

검사는 지나가고, Node는 NaN을 찍습니다.

실행 결과
NaN

b의 지금 타입은 number라 검사를 지나지만, 서연 칸이 없어 실행하면 b는 undefined이고 90 + undefined가 NaN입니다.

10번 줄을 const b = math['서연'] ?? 0;⁠으로 고칩니다.

interface Scores {
  [name: string]: number;
}

const math: Scores = {
  민지: 90,
  도윤: 85,
};
const a = math['민지'];
const b = math['서연'] ?? 0;
console.log(a + b);
실행 결과
90

흔한 오답은 '10번 줄에서 막힌다'입니다. 검사기가 math의 값을 보고 서연 칸이 없는 것을 안다고 본 것입니다. 검사기는 Scores 타입만 보고, 칸이 실제로 있는지는 모릅니다.

컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요. 2절 코드를 main.ts로 저장해 npx tsc로 검사하고 node main.ts로 돌립니다. 그다음 10번 줄의 : Sold를 지워 자바스크립트 코드와 똑같이 만들면 검사 결과가 어떻게 바뀔지 먼저 적고, 다시 검사해서 맞춰 봅니다. 검사하고 돌리는 법은 내 컴퓨터에서 검사하고 돌리기에 있습니다.

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

타입스크립트 목차로