유형 유틸리티 작성 - TypeScript의 공용체 유형에서 인터페이스 파생

이 게시물에서는 TypeScript에서 작고 일반적인 유형 유틸리티를 빌드할 것입니다.

실제 사용 사례로 작업하기 위해 비제네릭 코드를 제네릭 유형 유틸리티로 변환하여 더 많은 입력 유형과 함께 작동할 수 있으므로 더 재사용할 수 있습니다.

type Dispatcher = {
  [Message in Action as Message['type']]: Message extends { payload: any }
    ? (payload: Message['payload']) => void
    : () => void;
};


제네릭이 아닌 코드, 재사용하기 어려움 ⤴

이 목표를 달성하는 방법에는 여러 가지가 있습니다. 멋진 개발 경험을 제공하기 위해 템플릿 리터럴과 유형 추론을 활용하여 멋진 것을 선택할 것입니다.

type Dispatcher =
  ValuesAsCallbacks<UnionToKeyValue<Action, 'type:payload'>>;


재사용 가능한 일반 유형 유틸리티 ⤴

이제 빌드하는 방법을 살펴보겠습니다 🤔

2단계 프로세스



먼저 한 유형을 다른 유형으로 전환하는 프로세스를 분석해 보겠습니다. 다음 섹션에서 구현 및 자세한 설명을 살펴보겠습니다.

1️⃣ 키-값으로의 합집합

먼저 콜론으로 구분된 문자열 리터럴을 제공하고 키로 사용할 속성과 값으로 사용할 속성을 정의하여 유니온을 키-값 인터페이스로 전환합니다. 공용체의 항목에서 속성이 누락된 경우 이 사실을 never 유형으로 나타낼 것입니다.

type Action =
  | { type: 'reset' }
  | { type: 'setValue'; payload: string }
  | { type: 'setSelection'; payload: [number, number] | null }
  | { type: 'trimWhitespace'; payload: 'leading'|'trailing'|'both' };

type IntermediateResult = UnionToKeyValue<Action, 'type:payload'>;
         
┌───────────────────────────────────────────────────────────────────┐
 type IntermediateResult = {                                       
   reset: never;                                                   
   setValue: string;                                               
   setSelection: [number, number] | null;                          
   trimWhitespace: 'leading'|'trailing'|'both';                    
 }                                                                 
└───────────────────────────────────────────────────────────────────┘


이것이 콜백이 있는 인터페이스로 어떻게 바뀔지 이미 알 수 있습니까?

2️⃣ 콜백으로서의 값

둘째, 키-값 인터페이스를 키-콜백 인터페이스로 변환하고 있습니다. 여기서 후자의 콜백 함수는 전자의 값과 동일한 유형의 매개변수를 허용합니다.

type IntermediateResult = {
  reset: never;
  setValue: string;
  setSelection: [number, number] | null;
  trimWhitespace: 'leading'|'trailing'|'both';
}

type Dispatcher = ValuesAsCallbacks<IntermediateResult>;
         
┌───────────────────────────────────────────────────────────────────┐
 type Dispatcher = {                                               
   reset: () => void;                                              
   setValue: (payload: string) => void;                            
   setSelection: (payload: [number, number] | null) => void;       
   trimWhitespace: (payload: 'leading'|'trailing'|'both') => void; 
 }                                                                 
└───────────────────────────────────────────────────────────────────┘


구현 및 약간의 설명



키-값에 대한 결합



첫 번째 유틸리티 유형을 구현하는 방법을 살펴보겠습니다 🕵️

type UnionToKeyValue<
  T extends { [key: string]: any },  
  K extends string  
> = ...


(1) The util accepts an interface, or a union of interfaces, indexed by keys being strings or string literals as a first parameter.

(2) It also requires a second parameter to be a string, or a string literal.



... = K extends `${infer Key}:${infer Value}`  
  ? Key extends keyof T  
    ? ...
    : never
  : never;


(3) Key and Value never appeared as a type parameter of the util. Thanks to template literal types, we can infer both from the type provided as the second parameter, so long as it's a string or a string literal, and it contains a colon somewhere inside.

Given 'type:payload' provided as a second parameter, we're going to infer that Key is 'type' and Value is 'payload'.

If we don't find a colon in the second parameter, we fail to pattern-match and we return never.

(4) Next, with the inferred Key, we double-check it belongs to the key set of the first parameter, T — an interface, or a union of interfaces. If it does not belong there, we return never.



? { [A in T as A[Key]]: Value extends keyof A ? A[Value] : never }  
...


(5) Now, we use mapped types to iterate through T, which implies T must be a union of types rather than a single interface to make mapping possible. With an interface, we'd see the use of keyof there.

The use of as combined with indexed access A[Key] also gives a clue about elements of the union being interfaces and not primitive types such as undefined or null.

Finally, if A[Key] does not exist in the element, we're not going to include it in the output. We're not that strict with the Value, if A[Value] does not exist, we represent that as never.



최종 코드

type UnionToKeyValue<
  T extends { [key: string]: any },
  K extends string
> = K extends `${infer Key}:${infer Value}`
  ? Key extends keyof T
    ? { [A in T as A[Key]]: Value extends keyof A ? A[Value] : never }
    : never
  : never;


그것은 대부분의 무거운 작업이 완료된 것입니다!

콜백으로서의 값



인터페이스에서 인터페이스로 이동하는 것이 약간 더 쉬울 것입니다. 살펴보겠습니다.

type ValuesAsCallbacks<T extends { [key: string]: any }> = ...  


(1) This util accepts only one parameter, that must represent an interface with keys being strings or string literals and no constraints on the values.



... = {
  [K in keyof T]: T[K] extends never  
  ? () => void 
  : (payload: T[K]) => void;  
};


(2) T is an interface, we iterate through all keys of the provided type, represented as K. Now, we check if the value, T[K], has type never.

(3) If we detect the value is of the type never, we return a type representing a callback not accepting any parameters.

(4) Otherwise, we return a type representing a single-parameter callback, where the parameter has the same type as the value in the original interface.



최종 코드

type ValuesAsCallbacks<T extends { [key: string]: any }> = {
  [K in keyof T]: T[K] extends never 
  ? () => void 
  : (payload: T[K]) => void;
};


그것이 바로 인터페이스 조합을 콜백이 있는 인터페이스로 바꾸는 퍼즐의 마지막 조각입니다 🎉 우리가 해냈습니다!

완전한 코드



이 게시물의 전체 코드 목록은 다음에서 찾을 수 있습니다under this link.



TypeScript 놀이터에서 이 게시물의 재사용 가능한 유형 ⤴

최종 단어



이제 하나의 접근 방식을 탐색했으므로 다른 접근 방식(예: UnionAsKeyValue<T, K>을 두 개의 개별 유틸리티로 분할)을 시도하고 어떻게 찾는지 확인하는 것이 좋습니다.

행운을 빕니다!

좋은 웹페이지 즐겨찾기