Record Type과 Index Signature

2cham_ny·2024년 3월 13일
2

TIL

목록 보기
2/8

01 - WHAT

올해 초, 디자인 시스템이라는 것을 처음 접하고 이를 활용하여 개발을 진행했다. 오늘은 여러 컴포넌트를 정의하면서 사용했던 Record Type에 대해 톺아보려고 한다.

우선 타입스크립트 공식문서를 보자.

Record<keys, Type>은 타입스크립트의 유틸리티 타입으로 나와있다.

interface CatInfo {
	age: number;
	breed: string;
}

type CatName = 'miffy' | 'boris' | 'mordered';

const cats : Record<CatName, CatInfo> = {
	miffy : {age : 10, breed : 'Persian'},
	boris : {age : 5, breed : 'Maine Coon'},
	mordred : {age : 16, breed : 'British Shorthair'},
};

cats.boris;

위 코드가 공식문서에 나와 있는 코드이다.

Constructs an object type whose property keys are Keys and whose property values are Type. This utility can be used to map the properties of a type to another type.

위 문장을 해석해보자.

⇒ 속성 키가 Keys이고 속성 값이 Type인 개체 유형을 구성한다. 이 유틸리티를 사용하여 유형의 속성을 다른 유형에 매핑할 수 있다.

다시 간단히 정리해보자.

Record<key, Type>

⇒ 키가 key 타입이고 값이 Type 형태의 value 객체를 생성한다고 정리할 수 있다.

❗여기서 잠깐

Record와 관련하여 많이 언급되는 Index Signatures에 대해서도 톺아보자.

DND 프로젝트를 진행하면서 겪었던 에러 중 하나가 Index Signature랑 관련이 있었다. 그땐 급하게 해결하는 데에만 급급해서 공식 문서를 읽어보지 못했었기에 지금 정리해보려고 한다.

우선 공식 문서를 톺아보기 전에 Record 타입과 인덱스 시그니처를 예시로 함께 살펴보자.

  • 레코드 타입
    type SimpleRecord = Record<string, number>;
    
    const simpleRecord: SimpleRecord = {
    	age: 23,
    	grade: 4,
    };
    → 레코드 타입에서는 Key로 문자열 리터럴을 사용할 수 있다.
    type SimpleRecord = Record<string, number>;
    
    // 위 코드와 아래 코드는 동일한 역할을 한다.
    
    type Simple = {
    	[key:string]:number;
    }
    • 또다른 Record Type 예시
      type PlaceOptions = 'aaa' | 'bbb' | 'ccc';
      
      type PlaceInfo = {
      	name: string;
      	description: string;
      	url: string;
      }
      
      const places: Record<PlaceOptions, PlaceInfo> = {
      	aaa: {
      		name: 'aaa item',
      		descriptoin: 'hi aaa!',
      		url: '/aaa',
      	},
      	bbb: {
      		name: 'bbb item',
      		descriptoin: 'hi bbb!',
      		url: '/bbb',
      	},
      	ccc: {
      		name: 'ccc item',
      		descriptoin: 'hi ccc!',
      		url: '/ccc',
      	},
      };
  • 인덱스 시그니처
    type Simple = {
    	[key: string]: number;
    };
    
    const simple: Simple = {
    	age: 23,
    	grade: 4,
    };
    → 하지만 인덱스 시그니처에서는 Key 타입으로 문자열 리터럴 사용이 불가능하다. [key: SimpleType] : number; ⇒ 에러! [key in SimpleType] : number; ⇒ ok (mapped object type)

아직까진 100% 이해 못하겠다!

공식 문서를 톺아보자~

📌 Index Signature

정리하기에 앞서 질문을 던져보자.

❓인덱스 시그니처는 언제 사용할 수 있을까?

Sometimes you don’t know all the names of a type’s properties ahead of time, but you do know the shape of the values.

In those cases you can use an index signature to describe the types of possible values, for example:

공식 문서에 나와 있는 말을 보자.

가끔 우리는 타입 속성의 모든 이름들을 알지 못하지만 값의 shape에 대해 알고 있는 경우가 있다. 이러한 경우에 우리는 인덱스 시그니처를 사용하여 가능한 값들의 타입을 설명할 수 있다.

interface StringArray {
	[index: number]: string;
}

const myArray: StringArray = getStringArray();
const secondItem = myArray[1];

위 코드를 살펴보자.

StringArray 인터페이스를 보면 인덱스 시그니처가 적용된 것을 볼 수 있다. 이 StringArray는 number 타입으로 인덱싱되고 string을 리턴하는 형식이다.

❓근데 왜 사용할까?

여러 가지 레퍼런스를 찾아보다가 아래와 같은 아티클을 발견했다.

[TypeScript]인덱스 시그니처(Index Signature) 사용 방법

앞서 말했던 것처럼, 타입의 property들을 다 알진 못하더라도 property의 key type만 알아도 value들을 활용해 구현하고자 하는 기능을 수행할 수 있다.

위 아티클을 참고해서 작성해보자면,

function AFunction(param: {[key: string]: number}) {

}

다음과 같은 함수 형태에서 파라미터에서 타입으로 체킹이 가능하다. key 타입이 string이 아니면서 value 타입이 number가 아니라면, 객체 property에 접근이 불가하기에 함수 실행과 동시에 체킹이 가능하다는 뜻이다.

인덱스 시그니처 property에 허용되는 것들은 다음과 같다.

→ string, number, symbol, template string patterns, union types

두 가지 유형의 indexers를 모두 지원이 가능하다고 한다. 무슨 말인지 살펴보자.

interface Animal {
	name: string;
}

interface Dog extends Animal {
	breed: string;
}

interface NotOkay {
	[x: number]: Animal;
	// 'number' index type 'Animal' is not assignable to 'string' index type 'Dog'.
	[x: string]: Dog;
}

numeric indexer에서 반환된 type은 string indexer로부터 반환된 타입의 하위 타입이어야 한다.

그 이유가 뭘까?

→ number로 인덱싱을 할 때, 자바스크립트는 실제로 object로 인덱싱하기 전에 string으로 변환한다. 이 말은 즉, number 타입의 100으로 인덱싱된 것이 string으로 인덱싱된 “100”과 같다는 뜻이다. 그래서 이 두 경우는 일관성이 필요하다.

또 다른 코드를 보자.

interface NumberDictionary {
	[index: string]: number;
	
	length: number; // ok
	name: string;
	// property 'name' of type 'string' is not assignable to 'string' index type 'number'.
}

obj.property는 또한 obj[”property”]로도 사용이 가능하다.

위 코드에서, name의 타입은 string index의 타입(number)과 일치하지 않아서 에러가 발생한다.

하지만, 다른 타입들의 속성들은 허용 가능하다. (당연한 얘기)

interface NumberOrStringDictionary {
	[index: string]: number | string;
	length: number; // ok
	name: string; // ok
}

추가적으로, 인덱스 할당을 방지하기 위해 readonly를 추가할 수 있다.

interface ReadonlyStringArray {
  readonly [index: number]: string;
}
 
let myArray: ReadonlyStringArray = getReadOnlyStringArray();
myArray[2] = "Mallory";
// Index signature in type 'ReadonlyStringArray' only permits reading.

readonly를 설정하게 되면 myArray[2]처럼 set할 수 없다.

  • Record Type의 pros and cons
    • props
      • 타입의 일관성
      • 자동 타입 체킹
      • 코드 자동 완성
      • 리팩토링의 용이
    • cons
      • 추가적인 코드 작성이 필요할 수 있다.
  • Index Signature의 pros and cons
    • pros
      • 유연성 → 객체에 동적으로 프로퍼티 추가가 가능하므로 코드의 유연성이 향상된다.
      • 일반화된 접근 방식 → 객체의 속성에 접근하는 일반화된 방법을 제공하므로 코드가 보다 간결해진다.
    • cons
      • 혼동 가능성
      • 타입 안정성 → 잘못된 사용으로 컴파일러가 오류를 감지하지 못할 수 있다.

02 - HOW

이 에러를 처음 발생했던 이유는 string을 key로 사용하려고 했었기 때문이다.

위에서 정리한 것처럼 인덱스 시그니처를 사용해 이 에러를 방지할 수 있다.

03 - RETROSPECT

한번쯤은 정리해보고 싶었던 내용이었는데 이렇게 정리하게 되어서 다행이다. 추후 개발하다가 인덱스 시그니처를 활용할 일이 생기면 지금 글에 덧붙여서 예시도 써보면 좋을 것 같다.

04 - 오늘의 한마디

왕!

profile
😈 기록하며 성장하자!

0개의 댓글