[Typescript] Array와 Tuple의 length 타입 차이? + 제네릭 타입의 특정 프로퍼티 체크하는 방법

TL;DR;

  • &와 keyof를 사용하여 타입의 특정 프로퍼티를 체크할 수 있다.
  • Array의 length는 number
  • Tuple의 length는 숫자형 리터럴

length 프로퍼티 체크

제네릭 타입을 사용하는 중에 해당 타입의 특정 프로퍼티가 있는지를 체크하고 싶다면, & 인터섹션과 keyof를 활용하여 구현할 수 있다.

type AT = T['property' & keyof T]

원리는 간단하다.

  1. T 제네릭 타입에 keyof를 하면 T 타입의 프로퍼티가 유니온 타입으로 변환된다.
  2. 이 유니온 타입의 인터섹션을 내가 체크하고 싶은 프로퍼티 명으로 하면 해당 프로퍼티만 남는다. - 만약 인터섹션이 되지 않으면 never 타입이 된다.
  3. 2번에서 추출한 프로퍼티로 T 타입에 indexed access를 하여 해당 프로퍼티의 타입으로 최종 결정된다.
type AT = T['property' & keyof T]

type Example = {'property': number, 'rest': string}
type Example2 = {a: number}

type Foo = AExample // number

type Bar = AExample2 // never

위 예시에서 Foo 타입은

  1. AExample = Example['property' & keyof T] 타입으로 정의된다.
  2. Example['property' & keyof T]Example['property' & ('property' | 'rest')]와 같다.
  3. Example 타입에는 'property' 프로퍼티 속성이 있으므로 해당 프로퍼티 속성의 타입이 인터섹션의 결과로 남는다.
  4. Example['property']는 number 타입에 해당하므로 최종적으로 Foo는 number 타입으로 정의된다.

이러한 원리를 이용하여 아래와 같이 T 제네릭 타입에 length 프로퍼티를 포함하는지 여부를 확인하고 값 타입을 추출할 수 있다.

type AT = T['length' & keyof T]

Array와 Tuple의 length

length 프로터피를 지닌 대표적인 객체 타입은 Array와 Tuple이 있다. (Tuple도 Array의 일종이지만, 타입스크립트 시스템에서 Array 타입과 뚜렷한 차이가 있다.)

우선 Tuple의 예시다.

type ExampleA = A['length' & keyof A]

const example: Example[1, 2, 3] = 3
const example2: Example[1, 2, 3] = 1 // error!

undefined

타입을 확인해보면 Tuple의 경우 length 프로퍼티에 대해 숫자형 리터럴을 반환한다.

undefined

다음은 Array의 예시다.

type ExampleA = A['length' & keyof A];

const arr = [1, 2, 3];

const example: Exampletypeof arr = 1;

example의 타입은 숫자형 리터럴이 아닌, number 타입으로 정의된다.

undefined

왜 이런 차이가 발생할까?

타입스크립트 시스템에서 Tuple 타입의 가장 중요한 특징은, 순서와 길이, 내부 요소의 타입이 모두 고정되어 있는 특별한 Array라는 점이다.

https://www.typescriptlang.org/docs/handbook/2/objects.html#tuple-types

A tuple type is another sort of Array type that knows exactly how many elements it contains, and exactly which types it contains at specific positions.

따라서 타입스크립트 시스템 상에서는 길이가 고정되어 있는 Tuple의 length는 숫자형 리터럴로, 길이가 고정되어 있지 않고 동적으로 길이가 달라지는 Array의 length는 number 타입으로 결정된다.

번외) number와 숫자형 리터럴

그렇다면 number는 숫자형 리터럴이 될 수 있을까?

아래의 R 타입을 확인해보면 never로 결정되는 것을 통해 알 수 있듯, number가 숫자형 리터럴보다 이미 더 큰 범주의 타입이기 때문에 number를 숫자형 리터럴로 extends할 수 없다.

type R = number extends Example[1, 2, 3] ? number : never; // never

// 아래의 타입 정의와 같다.
type R = number extends 3 ? 3 : never; // never
undefined

그렇다면 반대는 어떨까?

당연히 숫자형 리터럴은 number 타입보다 좁은 범주의 타입이기 때문에 extends가 가능하다.

type ExampleA = A['length' & keyof A];

const arr = [1, 2, 3];

type R = 3 extends Exampletypeof arr ? number : never; // number

// 아래의 타입 정의와 같다.
type R = 3 extends number ? number : never; // number

실제 사용 예시

아래는 React에서 상태 관리 라이브러리로 널리 사용되는 zustand의 코드 예시이다.

 type MutateS, Ms = number extends Ms['length' & keyof Ms]
  ? S
  : Ms extends []
    ? S
    : Ms extends [[infer Mi, infer Ma], ...infer Mrs]
      ? MutateStoreMutatorsS, Ma[Mi & StoreMutatorIdentifier], Mrs
      : never

Mutate 타입은 두 가지 제네릭 타입을 받고 있는데, 그중 첫번째 분기 처리로 Ms 타입에 대해

  1. length 프로퍼티를 가지는 타입인지?
  2. length 프로퍼티의 값 타입이 number 타입인지? 를 체크하고 있다.
type MutateS, Ms = number extends Ms['length' & keyof Ms] ? ... : ...

Profile picture

Written by Kim Soon Yo

IT 생태계의 플랑크톤

Github Link