이펙티브 타입스크립트
동작 원리의 이해와 구체적인 조언 62가지
아이템10 객체래퍼타입 피하기
자바스크립트에는 객체 이외에 기본형 값들들에 대한 일곱가지 타입 string, number, boolean, null, udefined, symbol, bigint 이 있습니다. 기본형 ‘string’에는 메서드가 없지만, 메서드를 가지는 ‘String’ 객체 타입이 정의되어있습니다.
const str = "Hello Sian !";
const str = "Hello Sian !";
const res = str.toUpperCase();
console.log(res); //"HELLO SIAN !"
console.log(str); //"Hello Sian !" => 기본형 문자열은 그대로 유지된다.
기본형 string 은 toUpperCase() 메서드를 가지고 있는 것처럼 보입니다. toUpperCase()는 string 의 메서드가 아닙니다. 자바스크립트는 기본형과 객체 타입을 자유롭게 변환할 수 있습니다.
기본형에서 메서드를 사용할 때
- 기본형을 String 객체로 래핑하고
- 메서드를 호출하고
- 마지막에 래핑한 객체를 버립니다
런타임에 기능을 수정하는 기법 몽키-패치 를 통해 내부적인 동작들을 관찰 할 수 있습니다. 다음은 String 객체의 charAt() 메서드를 몽키패치하여 charAt()이 호출될 때마다 객체, 객체타입, 인덱스를 출력하는 예제입니다.
const originalCharAt = String.prototype.charAt;
String.prototype.charAt = function(pos){ //charAt() 오버라이딩
console.log(this, typeof this, pos); //[String: 'primitive'] object 3
return originalCharAt.call(this, pos);
};
console.log('primitive'.charAt(3)); //m
메서드 내의 this 는 string 기본형이 아닌 String 객체 래퍼인 것을 확인할 수 있습니다. string 뿐만 아니라 다른 기본형에도 객체 래퍼 타입이 존재합니다.
number Number
boolean Boolean
symbol Symbol
bigint BigInt
타입스크립트는 기본형과 객체 래퍼 타입을 별도로 모델링합니다. string 을 매개변수로 받는 메서드에 String 객체를 전달하면 문제가 발생합니다.
function isGreeting(phrase: String) {
return ['hello', 'good day'].includes(phrase)
// ~~~~~~
// Argument of type 'String' is not assignable to parameter
// of type 'string'.
// 'string' is a primitive, but 'String' is a wrapper object;
// prefer using 'string' when possible
}
타입스크립트가 제공하는 타입선언은 전부 기본형 타입으로 되어있고, string 은 String 에 할당할 수 있지만 String 은 string 에 할당할 수 없습니다.
요약
- 기본형 값에 메서드를 제공하기 위해 객체 래퍼타입이 어떻게 쓰이는지 이해해야합니다
- 객체 래퍼 타입을 지양하고, 기본형 타입을 사용해야합니다.
- 기본형 타입은 객체 래퍼에 할당할 수 있기 때문에 타입스크립트는 기본형 타입을 객체 래퍼에 할당하는 선언을 허용합니다. 그러나 기본형 타입을 객체 래퍼에 할당하는 구문은 오해하기 쉽고, 굳이 그렇게 할 필요도 없습니다(아이템19)
아이템11 잉여 속성 체크의 한계 인지하기
타입이 명시된 변수에 객체 리터럴을 할당할 때 타입스크립트는 해당 타입의 속성이 있는지, 그리고 그 외의 속성은 없는지 확인합니다.
interface Room {
numDoors : number;
ceilingHeightFt : number;
}
const r : Room = {
numDoors: 1,
ceilingHeightFt : 10,
elephant : 'present',
};//오류메세지 : 개체리터럴은 알려진 속성만 지정할 수 있으며 'Room' 형식에 elephant 가 없습니다.
구조적 타이핑관점으로 생각해보면 오류가 발생하지 않아야 합니다.
interface Room {
numDoors : number;
ceilingHeightFt : number;
}
const obj = {
numDoors : 1,
ceilingHeightFt : 10,
elephant : 'present'
}
const r : Room = obj;
임시변수 obj 객체는 Room 타입 할당이 가능합니다. obj 타입은 Room 타입의 부분집합 (numbDoors, ceilingHeightFt) 을 포함하므로, Room 에 할당 가능하며 타입체커를 통과할 수 있습니다.
앞 두 예제의 차이점을 살펴보겠습니다. 예제 1에서는 구조적 타입 시스템에서 발생할 수 있는 오류를 잡을 수 있도록 ‘잉여속성 체크’ 라는 과정이 수행되었습니다.
잉여속성체크
객체 리터럴을 변수에 할당하거나 함수에 매개변수로 전달할 때 잉여속성체크가 수행됩니다.개체 리터럴에 포함되지 않은 속성이 객체에 할당되려고 할때, 타입스크립트는 이를 ‘잉여속성’ 이라고 판단하고 컴파일 오류를 발생시킵니다.
interface Member {
name : string
age : number
}
const person : Member = {
name : 'Sian',
age : 24,
unit : 'unit1' // 오류발생
}
Member 인터페이스에 unit 프로퍼티가 없기 때문에 컴파일러는 이를 잉여속성으로 판단하고 오류를 발생시켰습니다. 잉여속성체크를 이용하면 타입 시스템의 구조적 본질을 해치지 않으면서도 객체 리터럴에 알 수 없는 속성을 허용하지 않음으로 타입안전성을 보장할 수 있습니다.
interface Options{
title : string;
darkMode?:boolean;
}
const o : Options = {darkmode : true, title : 'Ski Free'}; //오류
//{darkmode : boolean; title:string;} 형식은 Options 형식에 할당할 수 없습니다. 개체 리터럴은 알려진 속성만 지정할 수 있지만, Options 형식에 darkmode 가 없습니다
객체 리터럴을 변수에 할당할 때 잉여속성체크가 수행되어 에러문구를 보여주고 있습니다.
Options 타입에 포함되지 않은 darkmode 속성이 할당되려고합니다. 타입스크립트는 darkmode 를 ‘잉여속성’ 이라고 판단하고 컴파일 오류를 발생시켰습니다. 잉여속성체크는 구조적 타이핑 시스템에서 허용되는 속성 이름의 오타 같은 실수를 잡는데 효과적인 방법입니다.
임시변수 를 도입하면 잉여속성체크를 건너뛸 수 있습니다
const intermediate = {darkmode : true, title : 'Ski Free'};
const o : Options = intermediate; //정상
변수 o 에는 객체 리터럴이 아닌 변수 intermediate 가 할당되었기에 잉여 속성 체크가 적용되지 않고, 오류는 사라집니다.
요약
- 객체 리터럴을 변수에 할당하거나 함수에 매개변수로 전달할 때 잉여 속성 체크가 수행됩니다.
- 잉여 속성 체크는 오류를 찾는 효과적인 방법이지만 타입스크립트 타입 체커가 수행하는 구조적 할당 가능성 체크와 역할이 다릅니다.
- 잉여 속성 체크에는 임시변수를 도입하면 잉여 속성 체크를 건너뛸 수 있다는 한계가 있습니다.
아이템12 함수 표현식에 타입 적용하기
자바스크립트(와 타입스크립트) 에서는 함수문장(statement) 과 함수 표현식(expression) 을 다르게 인식합니다.
함수 문장은 function 키워드로 선언되며, 코드블록 내 어디서든 호출할 수 있습니다.
function add(x: number, y: number) : number {
return x + y ;
}
함수 표현식은 변수에 할당된 함수 표현식으로 정의됩니다.
const add = function(x: number, y:number) : number {
return x + y;
};
타입스크립트에서는 함수표현식을 사용하는 것이 좋습니다. 매개변수부터 반환값 까지 전체를 함수 타입으로 선언하여 함수 표현식에 재사용할 수 있기 때문입니다.
function add (a : number, b: number) {return a + b; }
function sub (a : number, b: number) {return a - b; }
function mul (a : number, b: number) {return a * b; }
function div (a : number, b: number) {return a / b; }
type BinaryFn = (a : number, b:number) => number;
const add : BinaryFn = (a,b) => a+b;
const sub : BinaryFn = (a,b) => a-b;
const mul : BinaryFn = (a,b) => a*b;
const div : BinaryFn = (a,b) => a/b;
사칙 연산을 하는 함수 네개에서 반복되는 함수 시그니처를 하나의 함수 타입으로 선언해 사용했습니다. 타입구문이 적고, 함수 구현부도 분리되어있어 로직이 보다 분명해집니다.
시그니처가 일치하는 다른 함수가 있을 때 함수표현식에 타입을 적용해보겠습니다.
const responseP = fetch('/quote?by=Mark+Twain') // Type is Promise<Response>
웹 브라우저에서 fetch 함수는 특정 리소스에 HTTP 요청을 보냅니다.
async function getQuote() {
const response = await fetch('/quote?by=Mark+Twain')
const quote = await response.json()
return quote
}
/quote 가 존재하지 않는 API 라면 ‘404 Not Found’ 가 응답됩니다. 응답은 json 이 아닐 수 있으며 response.json() 은 JSON 형식이 아니라는 새로운 오류 메시지를 담아 거절된 프로미스를 반환합니다(rejected). 그리고 호출한 곳에서는 새로운 오류 메세지가 전달되어 실제 오류인 404 가 감춰집니다. 따라서 상태 체크를 수행해줄 함수를 작성하겠습니다.
const checkedFetch: typeof fetch = async (input, init) => {
const response = await fetch(input, init)
if (!response.ok) {
throw new Error('Request failed: ' + response.status)
}
return response
}
타입스크립트가 input 과 init 의 타입을 추론할 수 있도록 함수 전체에 typeof fetch 를 적용했습니다.
fetch 의 타입선언은 아래와 같습니다
declare function fetch(
input : RequuestInfo, init?: RequestInit
): Promise<Response>;
fetch 와 함수 시그니처 같은 checkedFetch 에 typeof 를 이용해 함수 타입을 적용하여 간결하게 작성했습니다.
요약
- 매개변수나 반환값에 타입을 명시하기보다는 함수 표현식 전체에 타입 구문을 적용하는 것이 좋습니다.
- 만약 같은 타입 시그니처를 반복적으로 작성한 코드가 있다면 함수 타입을 분리해내거나 이미 존재하는 타입을 찾아보도록 합니다.
- 다른 함수의 시그니처를 참조하려면 typeof fn 을 사용하면 됩니다.
아이템13 타입과 인터페이스의 차이점 알기
타입스크립트에서 타입을 정의하는 방법은 두가지가 있습니다.
- 타입type 키워드 사용
type TState = {
name : string;
capital: string;
}
- 인터페이스 사용
interface IState {
name: string;
capital: string;
}
클래스를 사용할 수도 있지만, 클래스는 값으로도 쓰일 수 있는 자바스크립트 런타임 개념입니다. 클래스가 타입으로 쓰일 때는 형태가 사용되는 반면, 값으로 쓰일 때는 생성자가 사용됩니다.
class Cylinder{
radius=1;
height=1;
}
function caculateVolume(shape: Cylinder){
if(shape instanceof Cylinder){ //객체가 특정 클래스에 속하는지 확인
shape //타입 : Cylinder
shape.radius //타입 : number
}
}
type 공간에 class 가 사용되면 형태만 쓰입니다. type 공간에 적혀진 Cylinder 는 컴파일 될 때 사라진다.Instanceof 뒤의 Cylinder 는 생성자를 의미합니다. 클래스는 타입으로도 사용될 수 있고 값으로도 사용될 수 있습니다.
인터페이스 선언과 타입선언의 비슷한 점
- 추가속성과 함께 할당한다면 동일한 오류가 발생합니다
const wyoming: TState = {
name: 'wyoming',
capital: 'Cheyenne',
population: 500_000
};
//오류메세지 : 개체리터럴은 알려진 속성만 지정할 수 있으며 TState (또는 IState) 형식에 population이 없다.
- 인덱스 시그니처를 사용할 수 있습니다
//타입
type TDict = {[key: string]: string};
//인터페이스
interface IDict {
[key: string]: string;
}
- 함수타입 정의가 가능합니다
//타입
type TFn = (x: number) => string;
const toStrT: TFn = x => '' + x;
//인터페이스
interface IFn{
(x: number): string;
}
const toStrI: IFn = x => '' + x;
- 제너릭이 가능합니다
type TPair<T> = {
first: T;
second: T;
}
interface IPair<T>{
first: T;
second: T;
}
- 서로 확장이 가능합니다
- type 을 extends 한 interface
- interface 를 extends 한 type
interface IStateWithPop extends TState{
population: number;
}
type TStateWithPop = IState & {population: number;};
인터페이스 선언과 타입선언의 차이점
- 유니온 타입은 있지만 유니온 인터페이스는 없습니다
type AorB = 'a' | 'b';
인터페이스는 타입을 확장할 수 있지만, 유니온은 할 수 없습니다. 그렇지만 유니온 타입을 확장하는 것이 필요할 때가 있습니다.
확장방법 1) Input | Output 타입을 갖는 객체를 표현하는 VariableMap 인터페이스
type Input = {};
type Output = {};
interface VariableMap {
[name: string]: Input | Output;
}
확장방법 2) 유니온타입에 name 속성을 붙인 타입 NamedVariable 타입
type NamedVariable = (Input | Output) & {name: string};
- type 키워드는 유니온이 될 수 있습니다
- 타입에서 튜플과 배열 타입을 간결하게 표현할 수 있습니다
type Pair = [number, number];
type StringList = string[];
type NamedNums = [string, ...number[]];
- 인터페이스는 보강이 가능합니다.
- 타입선언에는 사용자가 채워야하는 빈틈이 있을 수 있습니다. 선언병합을 지원하기 위해 반드시 인터페이스를 사용해야합니다.
- 프로퍼티가 추가되는 것을 원하지 않는다면 인터페이스 대신 타입을 사용할 수 있습니다.
//선언병합 예제
interface IState {
name: string;
capital: string;
}
interface IState {
population: number;
}
const wyoming:IState = {
name: 'Wyoming',
capital: 'Cheyenne',
population:500_000
};
요약
- 타입과 인터페이스의 차이점과 비슷한 점을 이해해야 합니다
- 한 타입을 type 과 interface 두 가지 문법을 사용해서 작성하는 방법을 터득해야 합니다
- 프로젝트에서 어떤 문법을 사용할지 결정할 때 한 가지 일관된 스타일을 확립하고, 보강 기법이 필요한지 고려해야 합니다
아이템14 타입 연산과 제너릭 사용으로 반복 줄이기
코드의 반복을 줄이는 가장 간단한 방법은 타입에 이름을 붙이는 것 입니다. 아래는 상수를 사용해서 반복을 줄이는 기법을 동일하게 타입 시스템에 적용한 것입니다.
function distance(a: { x: number; y: number }, b: { x: number; y: number }) {
return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2))
}
interface Point2D {
x: number
y: number
}
function distance(a: Point2D, b: Point2D) {
/* ... */
}
같은 시그니처를 공유하고 있는 함수는 명명된 타입으로 분리해내어 반복을 줄일 수 있습니다
function get(url: string, opts: Options): Promise<Response> {
/* COMPRESS */ return Promise.resolve(new Response()) /* END */
}
function post(url: string, opts: Options): Promise<Response> {
/* COMPRESS */ return Promise.resolve(new Response()) /* END */
}
type HTTPFunction = (url: string, options: Options) => Promise<Response>
const get: HTTPFunction = (url, options) => {
/* COMPRESS */ return Promise.resolve(new Response()) /* END */
}
const post: HTTPFunction = (url, options) => {
/* COMPRESS */ return Promise.resolve(new Response()) /* END */
}
한 인터페이스가 다른 인테페이스를 확장하게 해서 반복을 제거할 수도 있습니다
interface Person {
firstName: string
lastName: string
}
interface PersonWithBirthDate extends Person {
birth: Date
}
이미 존재하는 타입을 확장하는 경우에는 인터섹션 연산자(&)를 쓸 수도 있습니다
interface Person {
firstName: string
lastName: string
}
type PersonWithBirthDate = Person & { birth: Date }
전체 상태를 표현하는 State 타입과 부분만 표현하는 TopNavState가 있는 경우를 살펴보겠습니다.
interface State {
userId: string
pageTitle: string
recentFiles: string[]
pageContents: string
}
interface TopNavState {
userId: string
pageTitle: string
recentFiles: string[]
}
State를 인덱싱하여 중복을 제거할 수 있습니다
type TopNavState = {
userId: State['userId']
pageTitle: State['pageTitle']
recentFiles: State['recentFiles']
}
매핑된 타입을 사용해 반복되는 코드를 줄일 수 있습니다
type TopNavState = {
[k in 'userId' | 'pageTitle' | 'recentFiles']: State[k]
}
- 매핑된 타입
- 매핑된 타입은
in
키워드를 사용해 배열이나 튜플 등의 타입에 대해 루프를 도는 것과 같은 방식으로 새로운 타입을 만들어내는 기능이고, 주로 객체나 배열의 타입변환에 사용됩니다. - 이 패턴은 표준 라이브러리에서도 일반적으로 찾을 수 있으며, Pick 이라고 합니다.
- 매핑된 타입은
- Pick
- Pick 은 제네릭 타입으로 첫번째 인자로 객체타입을, 두번째 인자로 해당 객체 타입에서 추출하고자 하는 필드의 이름들을 문자열 리터럴 타입 배열로 받습니다.
- Pick 을 사용해서 State 에서 userId, pageTtiel, recentFiles 필드만 추출해서 간단하게 새로운 타입을 만들 수 있습니다.
type Pick<T, K> = { [k in K]: T[k] };
태그된 유니온에서도 다른 형태의 중복이 발생할 수 있습니다.
- 태그된 유니온은 유니온 타입에 문자열 리터럴 타입을 추가해서 타입 안전성을 높인 방법입니다
interface SaveAction {
type: 'save'
// ...
}
interface LoadAction {
type: 'load'
// ...
}
type Action = SaveAction | LoadAction
type ActionType = 'save' | 'load' // Repeated types!
Action 유니온을 인덱싱하면 타입 반복없이 ActionType 을 정의할 수 있습니다
type ActionType = Action['type'] // Type is "save" | "load"
type ActionRec = Pick<Action, 'type'>; //{type: "save" | "load"}
이때 ActionType 은 문자열 유니온이고, Pick 은 객체인 것이 다릅니다.
코드의 반복을 줄일 수 있는 키워드를 더 알아보겠습니다.
keyof
keyof 는 타입을 받아서 속성 타입의 유니온을 반환합니다
interface Options{
width: number;
height: number;
ocolor: string;
label: string;
}
interface OptionsUpdate{
width?: number;
height?: number;
color?: string;
label?: string;
}
다음 매핑된 타입과 keyof 를 사용해서 만든 OptionsUpdate 는 위의 OptionsUpdate와 완전히 동일합니다.
type OptionsUpdate = {[k in keyof Options]?: Options[k]};
typeof
값의 형태에 해당하는 타입을 정의하고 싶을 때 사용합니다.
- 사용전
const INIT_OPTIONS = {
width: 640,
height: 480,
color: '#00FF00',
label: 'VGA',
};
interface Options{
width: number,
height: number,
color: string,
label: string,
}
- 사용후
const INIT_OPTIONS = {
width: 640,
height: 480,
color: '#00FF00',
label: 'VGA',
}
type Options = typeof INIT_OPTIONS
ReturnType
- 함수나 메서드의 반환값에 명명된 타입을 만들고 싶을 수 있습니다.
- ReturnType 을 사용해 함수의 반환 타입을 변수로 선언하거나 다른 타입의 매개변수로 전달할 수 있습니다.
function getUserInfo(userId: string){
//...
return {
userId,
name,
age,
height,
weight,
favoriteColor
};
}
type UserInfo = ReturnType<typeof getUserInfo>;
위 코드에서 ReturnType 은 함수의 타입인 typeof getUserInfo 에 적용되었습니다. 적용대상이 값인지 타입인지 정확하게 알고 구분해서 처리해야 합니다.
제너릭타입
제너릭 타입은 타입을 위한 함수와 같습니다. 제네릭 타입은 타입스크립트에서 DRY 원칙을 적용하는 핵심 방법 중 하나입니다. 제너릭을 사용하면 타입의 중복을 최소화하고 재사용성을 높일 수 있습니다.
아래 두 함수는 각각 두개의 인수를 받아서 그 값을 합쳐서 반환합니다
function addNumbers(a: number, b: number): number {
return a + b;
}
function concatenateStrings(a: string, b: string): string {
return a + b;
}
이를 제너릭으로 변경할 수 있습니다
function combine<T>(a: T, b: T): T {
return a + b;
}
combine 함수는 T
라는 타입의 매개변수를 사용합니다. T
는 함수를 호출할 때 전달된 값의 타입으로 결정됩니다. 따라서 이 함수는 어떤 타입의 값을 합쳐서 반환할 수 있습니다.
제너릭 타입에서는 매개변수를 제한할 수 있는 방법이 필요합니다. (타입안전성을 보장하기 위해서) 제너릭 타입을 사용하면 여러 종류의 값들을 다룰 수 있는데, 제너릭 타입이 특정한 종류의 값만 다루도록 제한해야 하는 경우가 있습니다. extends 를 이용하면 제너릭 매개변수가 특정 타입을 확장한다고 선언할 수 있고 이를 통해 매개변수를 제한할 수 있습니다.
interface Name{
first: string;
last: string;
}
type DancingDuo<T extends Name> = [T,T];
제네릭 타입 매개변수 T 는 Name을 확장합니다. DancingDuo 타입은 T 타입의 배열이고, 요소는 Name 타입 객체로 이뤄져있습니다.
const couple1: DancingDuo<Name> = [
{first: 'Fred', last: 'Astaire'},
{first: 'Ginger', last: 'Rogers'}
]; //정상
const couple2: DancingDuo<{first: string}> = [
{first: 'Sonny'},
{first: 'Cher'}
]; // 오류메세지 : Name 타입에 필요한 last 속성이 {first: string;} 타입에 없습니다.
{first: string} 은 Name 을 확장하는 것이 아니기 때문에 오류가 발생합니다.
Pick
type Pick<T, K extends keyof T> = {
[k in K]: T[k];
};
Pick 타입은 T 타입에서 K 타입에 포함된 속성만 선택해 새로운 타입을 정의합니다. 이 때 in 키워드를 사용한 매핑타입을 이용해서 새로운 객체 타입을 생성합니다.
요약
- DRY 원칙을 타입에도 최대한 적용해야합니다.
- 타입에 이름을 붙여서 반복을 피하고, extends 를 사용해서 인터페이스 필드의 반복을 피해야합니다.
- 타입들간의 매핑을 위해 매핑된 타입 keyof,typeof,인덱싱,매핑된 타입들을 공부해야합니다.
- 제너릭 타입은 타입을 위한 함수와 같습니다. 타입을 반복하는 대신 제너릭 타입을 사용하여 타입들 간에 매핑을 하는 것이 좋습니다.
아이템15 동적데이터에 인덱스 시그니처 사용하기
타입스크립트에서는 타입에 인덱스 시그니처 를 명시해 매핑을 유연하게 표현합니다.
type Rocket = {[property: string]: string};
const rocket: Rocket = {
name: 'Falcon 9',
variant: 'v1.0',
thrust: '4,940 kN',
};
위 코드에서 인덱스 시그니처 [property: string]: string 는 다음 의미를 갖고 있습니다.
- 키의 이름 : 키의 위치만 표시하는 용도
- 키의 타입 : string | number | symbol
- 값의 타입 : 어떤것이든 될 수 있음
위 코드에서 발생할 수 있는 문제점은 아래와 같습니다.
- 잘못된 키를 포함해 모든 키를 허용합니다.
const rocket: Rocket = {
Name: 'Falcon 9', //name 대신 Name으로 작성해도 유효하다
variant: 'v1.0',
thrust: '4,940 kN',
};
- 특정키가 필요하지 않습니다.
const rocket1: Rocket = {} //정상
- 키마다 다른 타입을 가지는 것이 불가합니다
const rocket: Rocket = {
Name: 'Falcon 9',
variant: 'v1.0',
thrust: 4940,
};
=> 인덱스 시그니처를 인터페이스로 변경하면 단점들을 보완하고 타입스크립트에서 제공하는 자동완성, 정의로 이동, 이름바꾸기 등의 언어 서비스를 모두 사용할 수 있게 됩니다.
interface Rocket {
name: string;
variant: string;
thrust_kN: number;
}
const falconHeavy: Rocket = {
name: 'Falcon',
variant: 'v1.0',
thrust_kN: 15_200
}
언제 인덱스 시그니처를 사용해야할까요 ?
인덱스 시그니처는 동적 데이터를 표현할 때 사용합니다. 동적 속성을 포함하는 객체를 다루는 경우, 타입스크립트는 해당 속성의 타입을 정적으로 파악할 수 없기 때문에 타입검사를 수행할 수 없습니다. 따라서 이러한 경우엔 인덱스 시그니처를 사용해서 해당 속성의 타입을 정의할 수 있습니다.
예를들어, 데이터 행을 열 이름과 값으로 매핑하는 개체로 나타내고 싶은 경우가 있습니다. 일반적으로 열 이름이 무엇인지 미리 알 방법이 없습니다. 이럴 때 인덱스 시그니처를 사용합니다.
function parseCSV(input: string): { [columnName: string]: string }[] {
const lines = input.split('\n')
const [header, ...rows] = lines
return rows.map(rowStr => {
const row: { [columnName: string]: string } = {}
rowStr.split(',').forEach((cell, i) => {
row[header[i]] = cell
})
return row
})
}
어떤 타입에 가능한 필드가 제한되어 있는 경우라면 인덱스 시그니처로 모델링하지 말아야 합니다.
interface Row1 {[column: string]: number} //광범위하다
string 타입이 너무 광범위해서 인덱스 시그니처를 사용하는데 문제가 있습니다. 첫번째 대안으로는 Record 제너릭 타입이 있습니다.
Record
- 키 타입에 유연성을 제공하는 제너릭 타입입니다
- string 의 부분집합을 사용할 수 있습니다
Record<K, T> // K : 속성 이름의 타입, T : 속성 값의 타입
type Vec3D = Record<'x' | 'y' | 'z', number>;
두번째 대안으로는 매핑된 타입을 사용할 수 있습니다.
- 키마다 별도의 타입을 사용하게 해줍니다
type ABC = {[k in 'a' | 'b' | 'c']: k extends 'b' ? string : number};
요약
- 런타임때까지 객체의 속성을 알 수 없을 경우에만 인덱스 시그니처를 사용하도록 합니다.
- 안전한 접근을 위해서 인덱스 시그니처의 값 타입에 undefined 를 추가하는 것을 고려해야 합니다.
- 가능하다면 인터페이스, Record, 매핑된 타입 같은 인덱스 시그니처보다 정확한 타입을 사용하는 것이 좋습니다.