이펙티브 타입스크립트:2장 아이템10-15

이펙티브 타입스크립트 북스터디 2장 아이템10-15

이펙티브 타입스크립트

동작 원리의 이해와 구체적인 조언 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 의 메서드가 아닙니다. 자바스크립트는 기본형과 객체 타입을 자유롭게 변환할 수 있습니다.

기본형에서 메서드를 사용할 때

  1. 기본형을 String 객체로 래핑하고
  2. 메서드를 호출하고
  3. 마지막에 래핑한 객체를 버립니다

런타임에 기능을 수정하는 기법 몽키-패치 를 통해 내부적인 동작들을 관찰 할 수 있습니다. 다음은 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, 매핑된 타입 같은 인덱스 시그니처보다 정확한 타입을 사용하는 것이 좋습니다.

디지엠유닛원 주식회사

  • 대표이사 권혁태
  • 개인정보보호책임자 정경영
  • 사업자등록번호 252-86-01619
  • 주소
    서울특별시 금천구 가산디지털1로 83, 6층 601호(가산동, 파트너스타워)
  • 이메일 web@dgmit.com