자바스크립트 객체는 문자열 키를 타입의 값에 관계없이 매핑한다.
타입스크립트에서는 타입에 인덱스 시그니처를 명시하여 유연하게 매핑을 표현할 수 있다.
type Rocket = {[property: string] : string}
const roket : Rocket = {
name: 'roket1',
variant: 'v1.0',
thrust: '4,940 kN'
}
위와 같이 타입 체크가 수행되면 네 가지 단점이 있다.
이러한 단점들로 인해 인덱스 시그니처는 보통 동적인 데이터에 대한 타입을 선언할 때만 주로 사용된다.
즉 런타임 때까지 객체의 속성을 알 수 없는 경우에 인덱스 시그니처를 사용한다.
예를 들어 CSV 파일처럼 헤더 행에 열 이름이 있고 데이터 행을 열 이름과 값으로 매핑하는 객체로 나타내고 싶을 경우 인덱스 시그니처를 유용하게 사용할 수 있다.
function parseCSV(input: string): {[columnName: string]: string}[] {
const lines = input.split('\n');
const [header, ...rows] = lines;
const headerColums = header.split(',');
return row.map(rowStr => {
const row: {[columnName: string]: string} = {};
rowStor.split(',').forEach((cell, i) => {
row[headerColums[i]] = cell;
});
return row;
});
}
선언해 둔 열들이 런타임에 실제로 일치한다는 보장은 없으므로 이 부분이 걱정된다면 undefined를 추가할 수 있다.
물론 모든 행들에 대한 데이터 타입을 알고 있을 경우에는 interface나 type을 사용하는 것이 더 안전하다.
자바스크립트에서 객체란 키/값 쌍의 모음이다. 이 때 키는 ES2015 이후로는 심벌도 가능하지만 주로 문자열이다. 숫자는 키로 사용할 수 없다. 하지만 타입스크립트는 숫자 키를 허용하며 문자열 키와 다른 것으로 인식한다.
const xs = [1, 2, 3]
const keys = Object.keys(xs) // 타입이 string[]
for(const key in xs) {
key;// 타입이 string
const x = sx[key] // 타입이 number
}
string은 number에 할당될 수 없으므로 위 코드의 마지막 줄에서 type error가 발생할 것이라고 생각할 수 있지만 이는 배열을 순회하는 코드 스타일에 대한 실용적인 허용이다.
하지만 위 코드는 배열을 순회하기에 좋은 방법은 아니다. 배열을 순회할 때 인덱스에 신경 쓰지 않아도 된다면 for in 보다는 for of 를 사용하는 것이 더 좋다.
인덱스의 타입이 중요한 경우 forEach나 for(;;) 루프를 사용하는 것이 좋다. 타입이 불활실할 경우 for in 은 이 세 가지 방법보다 몇 배나 느리기 때문이다.
또한 인덱스 시그니처에 number를 사용하기보다 Array나 튜플, 또는 ArrayLike 타입을 사용하는 것이 좋다.
타입스크립트에는 readonly
라는 키워드가 존재한다. 이름에서 알 수 있듯 함수가 매개변수로 받는 값을 변경없이 그대로 사용해야 할 경우 유용하게 사용할 수 있다.
function arraySum(arr: number[]): number {
let sum =0, num;
while((num = arr.pop()) !== undefined) {
sum += num;
}
return sum;
}
위 코드는 배열이 주어졌을 때 배열 안의 원소를 모두 더하는 코드이다. 그런데 계산이 끝나면 원래 배열이 전부 비게 되는 의도하지 않은 결과가 발생하게 된다. 자바스크립트 배열은 pop(), push()
등의 배열을 속성을 변경하는 메서드를 통해 배열의 내용을 변경할 수 있기 때문이다.
이처럼 의도하지 않은 오류를 발생시키지 않기 위해 readonly
키워드를 사용할 수 있다.
function arraySum(arr: readonly number[]): number {
let sum =0, num;
while((num = arr.pop()) !== undefined) { // ❗ readonly number[] 형식에 pop 속성이 없습니다.
sum += num;
}
return sum;
}
위의 코드는 실행시 readonly 배열에는 pop
속성이 없다는 에러를 출력한다. 이 때 readonly number[]
는 타입으로 number[]
와 구분되는 특징이 있다.
readonly
키워드는 다음과 같이 작동한다.
만약 함수가 매개변수를 변경하지 않는다면, readonly
로 선언해야 하며 어떤 함수를 readonly
로 만들면 그 함수를 호출하는 다른 함수도 모두 readonly
로 만들어야 한다.
const
와 readonly
는 초기 할당된 값을 변경할 수 없다는 공통점이 있지만 몇 가지 다른 점이 존재한다.
먼저 const
는 변수 참조를 위한 것이며 readonly
는 속성을 위한 것이다.
const constObject = {
property: 'foo'
}
constObject.property = 'bar' // ⭕ 변경 가능
type ReadonlyObject = {
readonly property : string;
}
const readonlyObject : ReadonlyObject = {
property: 'foo'
}
readonlyObject.property = 'bar' // ❌ cannot assign to 'property' because it is a read-only property
하지만 readonly
는 얕게 동작하므로 객체의 readonly
배열이 있다고 해서 그 객체 자체가 readonly
인 것은 아니다. 그러므로 깊은 readonly 타입을 사용하고 싶다면 제너릭을 직접 만들거나 ts-essentials
에 있는 DeepReadonly
제너릭을 사용하면 된다.