[TypeScript] Conditional Type & Distributed Condition Type

Hoplin·2023년 8월 11일
0
post-thumbnail

해당 글은 TypeScript Document를 해석하고 정리한 글입니다

Conditional Type

JavaScript(뿐만 아니라 TypeScript)에서 삼항 연산자는 자주 사용되는 연산자이다. 일반적인 형태는 아래와 같다

(condition) ? (Value of condition is true) : (Value of condition is false)

TypeScript의 Type 식을 작성할때도 조금은 다르지만 삼항연산자를 통해 입력에 따른 타입을 결정할 수 있으며, 이러한것을 Conditional Type이라고 지칭한다.

Conditional Type은 아래와 같은 형태를 띄고있다.

(target type) extends (some type) ? (type of true) : (type of false)

일반적인 조건문을 통해 판별하는 삼항연산자와 달리 Conditional Type은 extends키워드를 통해 할당 가능한지의 여부에 따라 타입을 결정하는것을 볼 수 있다. Conditional Type의 예시를 작성해본다.

interface Animal {
  walk(): void
}

class Cat implements Animal {
  public walk(): void {}
}

class Something {}

type IsItAnimal<T> = T extends Animal ? number : string

type Ex1 = IsItAnimal<Cat> //type Ex1 = number
type Ex2 = IsItAnimal<Something> //type Ex2 = string

위 예시에서는 IsItAnimal이라는 Conditional Type을 정의했다. T타입이 Animal타입에 할당이 가능한지 검사하고 가능하면 number 타입으로 추론, 불가능하면 string 타입으로 추론하는것을 볼 수 있다. Cat의 경우에는 Animal을 implements하기에 할당이 가능하므로 number타입으로, Something은 아니므로 string타입으로 추론하는것을 볼 수 있다.

조건부 타입 제한

Type Guard는 타입의 범위를 좁혀준다. 예를 들면 모든 타입을 핸들링할 수 있는 any타입에서 string타입으로 좁히는것과 같이 말이다. Conditional Type을 통해서 Generic 타입의 타입을 구체화시킬 수 있다.

예를 들어 아래와 같은 예시가 있다고 가정하자.

type MessageOf<T> = T["Message"]

위 코드는 에러를 반환한다.

'"Message"' 형식을 인덱스 형식 'T'에 사용할 수 없습니다.ts(2536)

이유는 TypeScript는 T타입이 "Message" 프로퍼티를 가지고 있다고 가정할 수 없기 때문이다. 만약 제네릭 안에 들어오는 타입에 "Message" 프로퍼티가 있으면, "Message" 프로퍼티의 값에 대한 타입을 반환한다고 가정해보자.

type MessageOf<T extends { Message: any }> = T["Message"]

위에서 정의한 MessageOf타입을 활용해서 예시를 작성해본다

interface Email {
  Message: string
}

interface PhoneCall {
  Signal: number
}

type MessageContent = MessageOf<Email>
type PhoneCallContent = MessageOf<PhoneCall>

위 예시에서는 PhoneCallContent에서 오류가 나는것을 볼 수 있다. 그 이유는 PhoneCall 인터페이스에는 Message 프로퍼티가 존재하지 않기 때문이다.

만약에 모든 타입을 받고, 조건에 충족하지 않는 경우 never를 반환하는 예시를 만들고 싶다면 아래와 같이 변형할 수 있다.

type MessageOfAny<T> = T extends { Message: any } ? T["Message"] : never
type MessageContent = MessageOfAny<Email>
type PhoneCallContent = MessageOfAny<PhoneCall>

위 예시에서 오류는 나지 않지만 PhoneCallContent타입이 never인것을 볼 수 있다.

infer키워드를 활용한 조건부 타입 내 추론

Conditional Type에서 "참"인 경우에서 infer키워드를 통한 타입 추론값 반환이 가능하다.(infer 키워드 사용. Inpa님의 블로그가 정리가 잘되어있다.) 만약에 배열타입인 경우, 배열의 요소의 타입을, 아닌경우에는, 해당 타입을 그대로 반환한다고 가정한다. 그러면 아래와 같이 타입을 정의할 수 있다.

type ElementType<T> = T extends Array<infer Element> ? Element : T

type NumberArrayElement = ElementType<Array<number>> // number
type SomeType = ElementType<string> // string

앞에서 말했듯이 infer 키워드를 통해 추론된 값은 "참"인 경우에만 추론값을 사용할 수 있다. 즉, "거짓"인 경우에는 infer 추론값을 사용할 수 없다는 말이다. 위의 예시 ElementType에서 추론된 타입 Element를 "거짓"인 경우의 반환 타입으로 변해보면, 오류가 나오는것을 볼 수 있다.

type ElementType<T> = T extends Array<infer Element> ? Element : Element // Error

'Element' 이름을 찾을 수 없습니다.ts(2304)

infer키워드를 활용하여 Return 타입을 추출하는 헬퍼클래스도 만들 수 있다.

type GetReturnValue<T> = T extends (...any: never[]) => infer Result
  ? Result
  : never

type Num = GetReturnValue<() => number> // number

type Str = GetReturnValue<() => string> // string

type Bol = GetReturnValue<() => boolean> // boolean

Distributed Condition Type

TypeChallenge 문제를 풀던 와중에 접하게 되었다

처음에 보고 이게 뭐지... 하고 있다가 TypeScript Document에서 Distributed Condition Type이라는 문서를 찾게되었다. 문서를 처음 봤을때 볼수 있는 문장은 이것이다.

제네릭 타입 위에서 조건부 타입은 유니언 타입을 만나면 분산적으로 동작합니다. 예를 들어 다음을 보겠습니다.

이 문장을 해석해보면 제네릭 타입에 Union Type을 받고, 타입 정의가 조건부 타입인 경우 각각의 Union Type에 대해 조건부를 따로따로 검사하게된다는 것이었다. 코드로 살펴보면 아래와 같다.

type ExampleType<T> = T extends any ? T[] : never

type Test1 = ExampleType<"a" | "b" | "c">

//type Test1 = "a"[] | "b"[] | "c"[]

ExampleType은 T라는 제네릭을 받고, any타입에 해당하면 T타입의 배열을, 아닌경우 never를 반환하게 된다. 그리고 Test1 타입에 ExampleType<"a" | "b" | "c"> 을 넣게되면, 각 문자열에 대해 조건 검사를 하는것을 볼 수 있다.

즉 ExampleType의 제네릭에 주어진 유니온타입 요소들은 각각 아래와 같이 매핑되는것이다.

ExampleType<"a"> | ExampleType<"b"> | ExampleType<"c">

위 TypeChallenge를 해설해본다. 문제는 아래와 같다

제네릭 타입에는 타입 T,U가 주어지며, T타입에서 U타입에 해당되는 것을 제외하는 것이다. 우선 정답은 아래와 같다

type ExcludeQ<T, U> = T extends U ? never : T

type Result = ExcludeQ<"a" | "b" | "c", "a">

우선 좌측에 있는 유니온타입 ("a" | "b" | "c") 각각에 대해 검사를 해 U타입에 해당하는지를 검사해야한다. 그렇기에 Distributed Conditional Type이 적용되어야 하는 타입은 T 타입이다.

그리고 U타입에 대입이 가능한 타입을 제외해야하기 때문에, T extends U 형식으로 조건을 만들어준다.

profile
더 나은 내일을 위해 오늘의 중괄호를 엽니다

1개의 댓글

comment-user-thumbnail
2023년 8월 11일

많은 것을 배웠습니다, 감사합니다.

답글 달기