일반 차별적 합집합 축소

19740 단어 typescriptprogramming

This post was originally a note on my Digital Garden 🌱



TypeScript에서 태그가 지정된 조합이라고도 하는 식별된 조합으로 작업할 때 종종 조합의 값을 멤버 중 하나의 유형으로 좁혀야 합니다.

type Creditcard = {tag: 'Creditcard'; last4: string}

type PayPal = {tag: 'Paypal'; email: string}

type PaymentMethod = Creditcard | PayPal


위의 유형이 주어지면 값이 사용자의 이메일을 표시하기 위해 PayPal 유형인지 또는 마지막 4자리를 표시하기 위해 신용카드 유형인지 확인하려고 합니다.

이 작업은 간단하지만 판별 속성(이 경우 tag)을 확인하면 됩니다. PaymentMethod 유형의 두 구성원은 각각 tag 속성에 대해 서로 다른 유형을 가지므로 TypeScript는 유형을 좁히기 위해 이들을 구별할 수 있습니다. 따라서 태그가 지정된 조합 이름입니다.

declare const getPaymentMethod: () => PaymentMethod

const x = getPaymentMethod()

if (x.tag === 'Paypal') {
  console.log("PayPal email:", x.email)
}

if (x.tag === 'Creditcard') {
  console.log("Creditcard last 4 digits:", x.last4)
}

tag 속성에 대한 이러한 명시적 검사를 피하기 위해 type predicates 또는 가드를 정의할 수 있습니다.

const isPaypal = (x: PaymentMethod): x is PayPal =>
  x.tag === 'Paypal'

const x = getPaymentMethod()

if (isPayPal(x)) {
  console.log("PayPal email:", x.email)
}


이러한 가드의 문제는 애플리케이션에서 식별된 각 공용체 유형의 각 멤버에 대해 정의해야 한다는 것입니다. 아니면 그들은?

나와 같다면 모든 공용체 유형에 대해 일반적으로 이 작업을 수행하는 방법이 궁금할 것입니다. 일부 TypeScript 유형 수준의 흑 마법으로도 가능할 수 있습니까?

짧은 대답은 '예'입니다.

const isMemberOfType = <
  Tag extends string,
  Union extends {tag: Tag},
  T extends Union['tag'],
  U extends Union & {tag: T}
>(
  tag: T,
  a: Union
): a is U => a.tag === tag


적절한 응답은 도대체 해킹이 뭐야?

알겠습니다. 설명하겠습니다.

네 가지 일반 유형이 필요합니다. 실제로 호출자는 이를 전달할 필요가 없으며 TypeScript가 유형 추론 비즈니스를 수행할 수 있도록 하는 목적이 있습니다.

그들 모두에 대해 어떤 유형도 허용하지 않고 대신 extends를 사용하여 해당 일반 유형이 특정 조건을 충족해야 함을 TypeScript에 알립니다.
  • TagUnion의 모든 태그의 합집합입니다. 태그의 기본 유형을 확장해야 합니다. 이 경우에는 string
  • Union는 태그가 지정된 공용체 유형이며 tag 유형의 속성을 가진 객체여야 합니다.
  • Tag는 범위를 좁히려는 특정 태그이므로 T 유형
  • 을 확장해야 합니다.
  • 그리고 Tag는 범위를 좁히려는 U의 특정 구성원이며, Union를 확장해야 하며 해당 태그는 Union, 원하는 특정 구성원
  • 이어야 합니다.
    TTag 는 좁히려는 공용체 유형을 정의하기 위해 존재하며 UnionT 는 더 나은 이름을 찾는 게으름이 없기 때문에 좁혀진 유형이므로 마법이 발생합니다. 우리는 얻을 것으로 기대합니다.

    const x = getPaymentMethod()
    
    if (isMemberOfType('Creditcard', x)) {
      console.log('Creditcard last 4 digits:', x.last4)
    }
    
    if (isMemberOfType('Paypal', x)) {
      console.log('PayPal email:', x.email)
    }
    


    Et voilà, 작동합니다!



    이것이 TypeScript가 제네릭의 구멍을 채우는 방법입니다.


    U의 변형이 있는데 유형 조건자 대신 일종의 getter로 작동하는 것을 사용하고 싶습니다.

    const getMemberOfType = <
      Tag extends string,
      Union extends {tag: Tag},
      T extends Union['tag'],
      U extends Union & {tag: T}
    >(
      tag: T,
      a: Union
    ): U | undefined =>
      isMemberOfType<Tag, Union, T, U>(tag, a) ? a : undefined
    


    사용법은 비슷하지만 물론 null 검사가 필요합니다.

    const cc = getMemberOfType('Creditcard', getPaymentMethod())
    
    if (cc) {
      console.log('Creditcard last 4 digits:', cc.last4)
    }
    
    const pp = getMemberOfType('Paypal', getPaymentMethod())
    
    if (pp) {
      console.log('PayPal email:', pp.email)
    }
    


    약간의 문제가 있습니다. 그것이 반환하는 유추 유형은 제네릭( isMemberOfType )에 대한 우리의 정의를 기반으로 하기 때문에 그리 좋지 않습니다.



    실제로 이것은 문제가 되지 않지만 여기까지 왔으니 계속 진행할 수 있습니다.

    TypeScript의 유형 유틸리티 중 하나인 Extract 을 입력합니다.

    Constructs a type by extracting from Type all union members that are assignable to Union.



    예를 들어 다음과 같은 경우 U extends Union & {tag: T}T0 유형이 됩니다.

    type T0 = Extract<'a' | 'b' | 'c', 'a' | 'f'>
    


    정의는 간단합니다.

    // Extract from Type those types that are assignable to Union
    type Extract<Type, Union> = Type extends Union ? Type : never
    



    'a'isMemberOfType 의 현재 정의에서 반환된 유형은 union: getMemberOfType 를 확장합니다.

    PayPal의 경우 U extends Union & {tag: T}가 됩니다. 반환된 유형에 PayPal & { tag: 'PayPal' }를 추가하면 대신 Extract를 얻을 수 있습니다.

    const isMemberOfType = <
      Tag extends string,
      Union extends {tag: Tag},
      T extends Union['tag'],
      U extends Union & {tag: T}
    >(
      tag: T,
      a: Union
    ): a is Extract<Union, U> => a.tag === tag
    
    const getMemberOfType = <
      Tag extends string,
      Union extends {tag: Tag},
      T extends Union['tag'],
      U extends Union & {tag: T}
    >(
      tag: T,
      a: Union
    ): Extract<Union, U> | undefined =>
      isMemberOfType<Tag, Union, T, U>(tag, a) ? a : undefined
    




    이 방법이 훨씬 깔끔합니다! 이제 안심하고 잠들 수 있습니다...

    결론적으로, 우리는 단순한 판별 속성 검사에서 동일한 결과를 달성하는 엄청난 제네릭 및 유형 추론으로 이동했습니다. 프로덕션 환경에서 이러한 유틸리티를 사용해야 합니까? 물론! 그것이 우리 동료들을 혼란스럽게 한다면 더욱 그렇습니다. 아닐 수도 있지만 TypeScript로 달성할 수 있는 멋진 것들을 발견하는 것은 즐거웠습니다.

    Here's the final version of the examples in the TypeScript Playground .

    좋은 웹페이지 즐겨찾기