08 모듈과 네임스페이스
09 실제 프로젝트에서 사용하기
10 유용한 라이브러리 소개

4.2 색인 가능 타입

동적인 색인을 표현하는 색인 가능 타입에 대해 다룬다.

앞서 다룬 예시들은 모두 코드 작성 시점에 속성 이름이 알려져 있었다. 하지만 코드의 실행 시점에서만 알 수 있는 이름의 동적 속성을 갖는 타입은 어떻게 표시해야 할까?

const users: = [
{ name: '안희종', height: 176, favoriteLanguage: 'TypeScript' },
{ name: '이방인', height: 42 }
];
interface NameHeightMap {
// ??
}
const nameHeightMap: NameHeightMap = {};
users.map(user => {
nameHeightMap[user.name] = user.height;
});
console.log(userHeightMap) // { '안희종': 176, 'Stranger': 42 }

위 코드의 nameHeightMap임의의 유저 목록을 받아 유저의 이름을 키로, 유저의 신장을 값으로 갖는 매핑이다. 이 때 이 객체의 키들은 임의의 유저 이름이므로 코드를 작성하는 시점에는 모든 가능한 키를 나열하는 것이 불가능하다. 예를 들어 위 타입을 아래와 같이 정의한다고 해 보자.

interface NameHeightMap {
안희종: number;
이방인: number;
}

이 경우 이후 users 값에 { name: '뉴페이스', height: 777 } 등의 유저가 추가되는 경우를 제대로 처리하지 못 할 것이다. 또한 실제로는 users 와 같은 정보를 실행 시간에 서버로부터 얻어오는 등의 경우가 많은데, 이런 경우 역시 커버할 수 없다. 이럴 때 필요한 것이 바로 색인 가능 타입(indexable type)이다.

색인 시그니쳐

색인 가능 타입을 이용해 색인 가능한(indexable) 객체의 타입을 정의할 수 있다. 색인 가능 타입을 정의하기 위해서는 색인에 접근할 때 사용하는 기호인 대괄호([])를 이용해 객체의 색인 시그니쳐(index signature)를 적어줘야 한다.

예를 들어 위의 NameHeightMap 인터페이스는 색인 가능 타입을 사용해 아래와 같이 적을 수 있다.

interface NameHeightMap {
[userName: string]: number | undefined;
}

위 정의는 다음과 같이 읽는다.

  • NameHeightMap 타입의 값을

  • 임의의 string 타입 값 userName 으로 색인한 값 ([userName: string] → 인덱스 시그니쳐)

  • nameHeightMap[userName]number 또는 undefined 타입의 값이다. (: number | undefined)

위 예제에선 색인된 값이 number 가 아닌 number | undefined 타입을 가지는 것에 유의하라. nameHeightMap이 모든 문자열을 키로 갖고 있다는 보장이 없으므로 nameHeightMap['없는 유저'] 따위의 값은 undefined 일 수 있기 때문이다.

이 경우 색인된 값을 number 타입의 값으로 사용하고 싶다면 먼저 undefined인지 여부를 체크해줘야 한다.

const h = nameHeightMap['안희종']; // 이 시점에서 h의 타입은 number | undefined
if (h !== undefined) {
// 이 시점에서 h의 타입은 number
console.log(h.toString()); // ok
}

색인과 타입

색인의 타입으로는 문자열 또는 숫자만이 사용 가능하다. 이 때 주의해야 할 점은 만약 문자열 색인과 숫자 색인이 모두 존재하는 경우, 숫자로 색인 된 값의 타입은 문자열로 색인 된 값 타입의 서브타입이어야 한다는 것이다.

즉, 아래 예제에서 BA의 서브타입이어야 한다.

inteface Mixed<A, B> {
[stringIndex: string]: A;
[numberIndex: number]: B;
}

이 때 “BA의 서브타입이다”는 말의 의미는 “B 타입의 모든 값은 A 타입에도 속한다” 정도로 이해할 수 있다. 예를 들어, 모든 정수를 나타내는 타입 Int와 모든 숫자를 나타내는 타입 Num이 존재한다고 하자. 모든 정수는 숫자이므로 (즉 Int 타입의 모든 값을 Num 타입의 값으로도 사용할 수 있으므로) IntNum의 서브타입이다.

이러한 제약이 존재하는 이유는 자바스크립트 색인의 동작 방식 때문이다. 자바스크립트 코드에서 객체의 색인에 접근할 때, 내부적으로는 색인의 toString() 메소드를 호출해 문자열로 변형된 값을 색인으로 사용한다. 예를 들어 1.toString() === '1' 이므로 obj[1] 이라고 적은 코드는 실제로는 obj['1']와 동일하다.

이 때, 만약 다음 ErrorProne 타입과 같이 숫자로 색인 된 값의 타입(boolean)이 문자열로 색인 된 타입(number)의 서브타입이 아닌 경우가 허용된다고 가정해보자.

interface ErrorProne {
[str: string]: number;
[num: number]: boolean;
}
let errorProne: ErrorProne = {
'abc': 3,
3: true
};
errorProne[3];

가장 아래 줄을 보면, 3이라는 색인은 숫자 타입이므로 타입 시스템은 errorProne[3]의 타입이 boolean일 것이라 추측할 것이다. 하지만 위에서 언급한 색인의 동작 방식에 의해 실제로 해당 값은 errorProne['3']과 같고, 이는 문자열 색인으로 접근한 number 타입의 값이다. 타입 시스템이 알고 있는 정보(boolean)와 실제 상황(number) 이 달라지는 것이다.

따라서 타입스크립트는 이런 코드를 작성하는 것을 허용하지 않고, error TS2413: Numeric index type 'boolean' is not assignable to string index type 'number'. 와 같은 에러를 발생시킨다. 숫자 색인으로 접근한 타입 boolean을 문자열 색인으로 접근한 타입 number에 할당할 수 없다는 의미다.

비슷한 이유로, 문자열 색인 시그니처가 존재한다면 그 외 모든 속성의 값 타입은 문자열 색인으로 접근한 값의 타입의 서브타입이여야 한다. 모든 속성 접근은 (user.name === user['name'] 이므로) 결국 문자열 색인 접근의 특수한 케이스이기 때문이다. 아래와 같은 선언은 타입 에러를 발생시킨다.

interface User {
[randomProp: string]: number;
name: string;
}
// error TS2411: Property 'name' of type 'string' is not assignable to string index type 'number'.

읽기 전용 색인

색인 역시 읽기 전용으로 선언할 수 있다. 객체 타입, 인터페이스에서의 readonly의 동작과 마찬가지로 readonly로 선언된 색인의 값은 재할당이 불가능하다.

interface ReadonlyNameHeightMap {
readonly [name: string]: height;
}
const m: ReadonlyNameHeightMap = { '안희종': 176 };
m['안희종'] = 177; // error TS2542: Index signature in type 'ReadonlyNameHeightMap' only permits reading.

색인 가능 타입의 사용예

색인 가능 타입을 사용하는 가장 간단하면서도 유용한 인터페이스 중 하나로 Array 인터페이스를 꼽을 수 있다. 만약 색인 가능 타입이 없이 T 타입의 원소를 갖는 Array 인터페이스를 작성한다면 대략 아래와 같은 식으로 모든 색인에 대한 타입을 일일이 정의해야 할 것이다.

interface Array<T> {
length: number;
0?: T;
1?: T;
/* ... */
Number.MAX_SAFE_INTEGER?: T;
/* 메소드 정의 */
}

인덱스 타입을 이용하면 위 코드를 다음처럼 간결하게 대체할 수 있다.

interface Array<T> {
length: number;
[index: number]?: T;
/* 메소드 정의 */
}