객체 타입
자바스크립트에서는 데이터를 묶고 주고받을 때 객체로 만들어서 사용합니다. 타입스크립트에서는 이러한 객체의 형태를 타입으로 표현합니다.
단순한 형태의 객체라면 문제가 되지 않지만, 복잡한 형태의 객체인 경우 타입으로 표현하는 것도 어려운 일이 됩니다. 복잡한 형태의 객체를 표현하기 위해 알아야 할 사항들과 방법에 대해 정리했습니다.
객체의 표현
객체 타입은 속성의 이름과 타입을 선언하여 표현합니다. type과 interface 두 가지 키워드를 사용하여 타입을 선언할 수 있습니다.
1interface User {
2 name: string;
3 email: string;
4}
5
6type Product = {
7 id: number;
8 title: string;
9};
10옵셔널 속성과 readonly
?를 추가해서 속성을 옵셔널로 표현할 수 있고, readonly를 추가해서 속성에 대한 재할당을 막을 수 있습니다.
1interface Config {
2 readonly apiKey: string; // 재할당 불가
3 timeout?: number; // 선택적 속성
4}
5인덱스 시그니쳐(Index Signature)
간혹, 객체가 가지는 모든 속성의 이름은 모르지만, 해당 속성들의 타입은 아는 경우가 있습니다. 이런 경우, 객체가 어떤 타입을 가지는 추가 속성을 얼마든지 가질 수 있도록 객체 타입을 표현할 수 있습니다.
인덱서의 타입은 string, number, symbol, 그리고 이 타입들의 유니온 타입으로만 표현할 수 있습니다.
1interface ColorInfo {
2 hex: string;
3 tone: string;
4}
5
6interface ColorTheme {
7 [index: string | number]: ColorInfo;
8}
9
10const appColorTheme: ColorTheme = {
11 warn: {
12 hex: '#FF0000',
13 tone: '생생한 빨간색',
14 },
15 primary: {
16 hex: '#2127DB',
17 tone: '선명한 네이비',
18 },
19 error: '#110011',
20 // Type 'string' is not assignable to type 'ColorInfo'.
21};
22복수 타입을 지원하는 인덱스 시그니쳐
복수의 타입을 가지는 인덱스 시그니쳐는 특이한 규칙이 있습니다. 숫자 인덱스 타입은 반드시 문자열 인덱스 타입의 서브타입만을 값 타입으로 가질 수 있습니다.
1interface Animal {
2 name: string;
3}
4
5interface Pet {
6 name: string;
7 owner: string;
8}
9
10// Pet은 Animal의 서브타입입니다.
11interface ValidIndexSignature {
12 [x: string]: Animal;
13 [x: number]: Pet;
14}
15
16const validIndexSignature: ValidIndexSignature = {
17 animal1: { name: 'animal1' },
18 1: { name: 'pet1', owner: 'me' },
19 2: { name: 'pet2', owner: 'you' },
20};
21
22interface InvalidIndexSignature {
23 [x: string]: Pet;
24 [x: number]: Animal;
25 // 'number' index type 'Animal' is not assignable to 'string' index type 'Pet'.
26}
27타입 호환성 관점에서, Pet(name, owner)은 Animal(name)의 서브타입입니다. Animal에서 요구하는 모든 속성을 Pet이 가지고 있기 때문입니다. 따라서 숫자 인덱스 타입이 문자열 인덱스 타입에 호환되어 타입 오류가 발생하지 않습니다. 하지만 반대의 경우에는 호환되지 않기 때문에 타입 오류가 발생합니다.
인덱스 시그니쳐는 다른 속성에도 영향을 미친다
1interface IndexSignatureAffectionTest1 {
2 [x: number]: string;
3 1: string[];
4 // Property '1' of type 'string[]' is not assignable to 'number' index type 'string'.
5 length: number;
6}
7
8interface IndexSignatureAffectionTest2 {
9 [x: string]: string;
10 1: string[];
11 // Property '1' of type 'string[]' is not assignable to 'string' index type 'string'.
12 length: number;
13 // Property 'length' of type 'number' is not assignable to 'string' index type 'string'.
14}
15인덱스 시그니쳐는 다른 속성에도 영향을 미칩니다. 앞서 언급한 것처럼, 숫자 타입 인덱서의 리턴 타입은 문자열 타입 인덱서의 리턴 타입의 서브타입이어야 합니다. 두 번째 예제에서는 인덱서의 타입이 string이지만 숫자 인덱서인 1도 영향을 받아 타입 오류가 발생합니다.
인덱스 시그니쳐에 readonly 키워드 사용
인덱스 시그니쳐에도 readonly 키워드를 사용할 수 있습니다. 이 경우, 속성을 추가하거나 수정할 수 없게 됩니다.
1interface ReadonlyIndexSignature {
2 readonly [x: string]: string;
3}
4
5const readonlyIndexSignature: ReadonlyIndexSignature = {
6 name: 'Tardis',
7 color: 'blue',
8};
9
10readonlyIndexSignature.owner = 'David Tennant';
11// Index signature in type 'ReadonlyIndexSignature' only permits reading.
12초과 속성 검사(Excess Property Checks)
어디서 어떻게 객체에 타입이 할당되었는지에 따라, 타입 시스템상에서의 차이가 발생할 수 있습니다. 이러한 차이의 대표적인 예시로 초과 속성 검사(Excess Property Checking)가 있습니다.
1interface Rocket {
2 name?: string;
3 color?: string;
4}
5
6function literalExample(rocket: Rocket) {
7 console.log(`name: ${rocket.name ?? 'unknown'}, color: ${rocket.color ?? 'unknown'}`);
8}
9
10literalExample({ name: 'Castle Bravo I' }); // OK
11
12literalExample({ name: 'Castle Bravo II', colour: 'Red' });
13// Object literal may only specify known properties,
14// but 'colour' does not exist in type 'Rocket'. Did you mean to write 'color'?
15
16const castleBravo = { name: 'Castle Bravo II', colour: 'Red' };
17literalExample(castleBravo); // OK - 초과 속성 검사가 발생하지 않음
18문제 상황
name, color 둘 다 옵셔널 속성이므로, 해당 속성이 없어도 컴파일 에러가 발생하지 않습니다. 하지만 객체 리터럴을 직접 전달하는 경우, colour이라는 없는 속성이 있다는 이유로 에러가 발생합니다.
한편, 똑같은 속성 값을 가지는 객체를 변수에 먼저 할당한 후 전달하면 에러가 발생하지 않습니다.
초과 속성 검사를 하는 경우
이렇게 동작하는 이유는, 객체 리터럴을 다른 변수에 할당하는 경우에 대해서만 초과 속성 검사를 하기 때문입니다.
타입스크립트 핸드북에 따르면, 타입스크립트는 객체 리터럴을 어떤 변수에 할당하려는데, 대상 타입에 없는 초과 속성을 가지고 있으면 버그일 수도 있다고 판단한다고 합니다.
| 상황 | 초과 속성 검사 |
|---|---|
| 객체 리터럴을 직접 할당 | 검사함 |
| 변수에 할당된 객체를 전달 | 검사하지 않음 |
초과 속성 검사를 피하는 방법
초과 속성 검사를 하더라도 컴파일 오류가 발생하지 않게 하려면, 인덱스 시그니쳐를 사용하면 됩니다.
1interface Rocket2 {
2 name?: string;
3 color?: string;
4 [propertyName: string]: unknown;
5}
6
7function literalExample2(rocket: Rocket2) {
8 console.log(`name: ${rocket.name ?? 'unknown'}, color: ${rocket.color ?? 'unknown'}`);
9}
10
11literalExample2({ name: 'Castle Bravo I' }); // OK
12literalExample2({ name: 'Castle Bravo II', colour: 'Red' }); // OK
13
14const castleBravo2 = { name: 'Castle Bravo II', colour: 'Red' };
15literalExample2(castleBravo2); // OK
16인덱스 시그니쳐 [propertyName: string]: unknown을 추가함으로써, 알려지지 않은 속성도 허용하도록 타입을 정의할 수 있습니다.
정리
이번 포스트에서는 인덱스 시그니쳐와 초과 속성 검사에 대해 알아보았습니다. 내용을 정리하면 다음과 같습니다.
| 개념 | 설명 |
|---|---|
| 인덱스 시그니쳐 | 속성 이름을 모르지만 타입을 아는 경우 사용 |
| 복수 인덱서 규칙 | 숫자 인덱서의 값은 문자열 인덱서 값의 서브타입이어야 함 |
| 초과 속성 검사 | 객체 리터럴 할당 시에만 발생 |
| 인덱스 시그니쳐 활용 | 초과 속성 검사를 우회할 수 있음 |
- 인덱스 시그니쳐를 사용하면 동적인 속성을 가진 객체 타입을 표현할 수 있다.
- 인덱스 시그니쳐는 다른 속성에도 영향을 미치며, 복수 인덱서 사용 시 서브타입 규칙을 따라야 한다.
- 초과 속성 검사는 객체 리터럴을 직접 할당할 때만 발생하며, 이는 타이핑 실수로 인한 버그를 방지하기 위한 것이다.
긴 글 읽어주셔서 감사합니다. 다음 포스트에서는 type과 interface의 차이점과 Mapped Type에 대해 알아보겠습니다.