Advance II/TypeScript

[TS] Index Signature

kinggora 2023. 9. 12. 00:06

인덱스 시그니처(Index Signature)

{ [ Key :  T ] :  U }

  • T: Key의 타입
  • U: Value의 타입

객체의 특정 value에 접근할 때 key를 문자열로 인덱싱하여 참조하는 방법이다. (Key-Value Structure)

Key와 Value의 타입을 정확히 명시해야 하는 경우 사용할 수 있다.

모든 멤버 변수의 규격을 정의하는 기능을 한다.

인덱스 시그니처 선언에서 key의 키워드는 임의로 바꾸어 사용할 수 있고, 동일한 키워드를 여러 개 사용할 수 있다.

 

Index Signature 선언

type objType = {
    [key: string]: number, // Index Signature: key는 string 이고 value는 number 라는 의미
    foo: number,
    bar: string; // **ERROR**
};

let obj: objType = {
    num: 1,
    arr: [1, 2, 3] // **ERROR**
};
let mySalary = {
    bonus: 200,
    pay: 2000,
    allowance: 100,
    incentive: 100
};

function totalSalary(salary: {[key: string]: number}) {
    let total = 0;
    for(const key in salary) {
        total += salary[key];
    }
    return total;
};

console.log(totalSalary(mySalary)); // 2400
  • totalSalary 함수는 속성의 key가 string 타입이고 value가 number 타입인 salary 객체를 매개변수로 받는다.
  • 함수 내부에서 salary 객체의 속성 값(salary[key])에 접근하여 연산을 수행한다.

사용 예

1. 객체 리터럴에 새로운 속성 추가

클래스가 인터페이스를 구현할 때 인터페이스에 정의되지 않은 새로운 속성이 추가되어도 오류를 발생시키지 않는다.

하지만 인터페이스를 타입으로 하는 객체 리터럴에서는 새로운 속성을 추가할 수 없다고 오류를 출력한다.

TypeScript는 기본적으로 인터페이스에 정의되지 않은 속성을 동적으로 할당하는 것을 오류로 정의한다.

interface ButtonInterface {
    onInit?():void;
    onClick():void;
}

// ButtonInterface 를 구현한 클래스
class ButtonComponent implements ButtonInterface {
    
    // onClick()을 구현하고 있으므로 오류 발생 X
    type:string = "button";
    disabled:boolean = false;
    
    constructor() {}
    onClick() {}
}

// ButtonInterface 을 타입으로 하는 객체 리터럴
const button:ButtonInterface = {
    
    type: "button",	// ERROR
    disabled: false,	// ERROR
    onClick() {}
};

이때 인덱스 시그니처를 사용하여 동적으로 추가할 속성을 명시하면 오류를 출력하지 않는다.

[prop:string]: any 추가

interface ButtonInterface {
    onInit?():void;
    onClick():void;
    [prop:string]: any;
}

// ButtonInterface 을 타입으로 하는 객체 리터럴
const button:ButtonInterface = {

    type: "button", 
    disabled: false, 
    onClick() {}
};

2. String literal 키가 아닌 String 키로 객체에 접근하기

TypeScript는 기본적으로 객체의 프로퍼티에 접근할 때 String literal 타입의 키를 허용한다.

String 키로 객체에 접근할 수 없다.

const obj = {
    foo: 'hello'
};

// string key
let propName1 = 'foo';
console.log(obj[propName1]); // ERROR

// string literal key
const propName2 = 'foo';
console.log(obj[propName2]); // hello
console.log(obj['foo']); // hello

Object.keys() 은 string[] 을 반환하므로 JS에서 사용하던 코드를 그대로 사용하면 컴파일 에러가 발생한다.

for (const key of Object.keys(obj)) {
  console.log(obj[key]) // error: key가 string 타입
}

해결 방법

  • type assertion: as 키워드 사용
const obj = {
  foo: 'hello',
};

let propName = 'foo';
console.log(obj[propName as 'foo']);
  • index signature
type ObjType = {
  [index: string]: string
}

const obj: ObjType = {
  foo: "hello",
  bar: "world",
}

// string literal
const propertyName1 = "foo"
// string
const propertyName2:string = "bar"

console.log(obj[propertyName1]); // hello
console.log(obj[propertyName2]); // world

3. Number  타입 인덱스 시그니처를 통해 배열 literal 방식으로 프로퍼티 할당하기

number 타입의 인덱스 시그니처를 선언하면 다음과 같이 배열 literal 방식으로 할당할 수 있다.

interface ArrayLikeType {
  [key: number]: string;
}

const obj: ArrayLikeType = ["hello", "world"];
/* obj는 아래와 같이 할당된다.
    const obj: ArrayLikeType = {
        0: "hello",
        1: "world"
    };
*/
console.log(obj[0], obj[1]); // hello world

 

String 타입 인덱스 시그니처를 혼용하여 사용하면 배열 literal 방식의 할당은 불가능하다.

interface ArrayLikeType {
    [key: number]: string;
    [key: string]: string;
}

let obj: ArrayLikeType = {
    0: "hello",
    foo: "world"
};
let foo = "foo";
console.log(obj[0], obj[foo]); // hello world

obj = ["hello", "world"]; // ERROR
  • 오류 내용: Type 'string[]' is not assignable to type 'ArrayLikeType'. Index signature for type 'string' is missing in type 'string[]'.
  • foo는 string 타입이지만 인덱스 시그니처를 사용하여 객체의 프로퍼티에 접근할 수 있다.

주의 사항

1. key의 타입은 string, number, symbol, Template literal 타입만 가능하다

type userInfoType = "name" | "age" | "address"; // string literal + union
type userType = {
    [key: string]: string | number | boolean;
    [key: userInfoType]: string; // error: An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.
};

const user: userType = {
    name: '홍길동',
    age: 20,
    isMale: true
};
  • 인덱스 시그니처의 타입으로 literal 이나 generic 타입은 사용할 수 없다.
  • 위와 같은 상황에서 IDE는 Mapped type을 권장하고 있다.

Index Signature와 Mapped Type

  • Mapped type은 인덱스 시그니처와는 관련이 없다.
// Mapped Type을 이용한 Index Signature 선언
type userInfoType = 'name' | 'age' | 'address';

type userType = {
  [key in userInfoType]: string; // in 키워드 사용
};
/* 위 코드는 아래 코드와 완전히 동일하므로 Index Signature와는 무관하다.
    type userType = {
      name: string,
      age: string,
      address: string
    };
*/
const user: userType = {
  name: '홍길동',
  age: '20',
  address: '서울',
  gender: 'male' // error
};

 

 

2. Key는 고유한 값으로 동일한 이름의 프로퍼티를 여러 개 가질 수 없다.

또한 동일한 타입의 인덱스 시그니처를 여러 개 선언할 수 없다. (키워드 이름과 무관)

type objType = {
    [key: string]: string | number,
    [field: string]: number, // error: Duplicate index signature for type 'string'
    [index: number]: string,
    length: number
};

const obj: objType = {
    key: "string",
    key: 1 // error: An object literal cannot have multiple properties with the same name.
}
  • length는 일반 프로퍼티. key가 string 이므로 value로 string 또는 number 값을 참조한다.
  • obj[key] = "string"? 1? (error)

3. 사용자 정의 타입을 인덱스 시그니처로만 정의하면 빈 객체를 할당해도 타입 에러가 발생하지 않는다.

type objType = {
    [key: string]: number | string,
    [index: number]: string
};

const obj: objType = {}
console.log(obj['prop']); // undefined

 

런타임에 객체의 속성(property)를 알 수 없는 경우에만 제한적으로 사용하는 것을 권장