RequireAtLeastOne 타입의 필요성
아래와 같은 인터페이스가 있다고 가정 해 봅시다.
아래의 인터페이스는 targetIdentifier를 통해 DOM의 특정 요소를 가져와서 extraClass 혹은 extraId를 추가할 수 있는 옵션을 정의합니다.
interface DOMElementIdentifierOptions {
targetIdentifier: ValidTargetIdentifier
extraClass?: string[]
extraId?: string
}
extraClass 혹은 extarId라는 값은 Optional 하긴 하지만, 반드시 둘 중 하나 이상은 필수인 인자가 됩니다.
이럴 때 필요한 것이 바로 아래에서 만들어 볼 RequireAtLeastOne Type 입니다.
Generic으로 RequireAtLeastOne 타입 구현
사실 해당 코드 자체는 이미 stackoverflow에 나와있습니다.
여기에서 해 볼 것은 각각이 무엇을 의미하는지, 그리고 최종적으로 어떻게 우리가 원하는 바를 구현 하는지 알아보도록 하겠습니다.
export type RequireAtLeastOne<T, Keys extends keyof T> =
Pick<T, Exclude<keyof T, Keys>>
& {
[K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>
}[Keys]
먼저 우리는 위 타입을 아래처럼 사용 할 것입니다.
export type ValidDOMElementIdentifierOptions = RequireAtLeastOne<DOMElementIdentifierOptions, 'extraClass' | 'extraId'>;
이제, 위에서 정의한 RequireAtLeastOne 이 어떻게 구성되는지 알아보도록 하겠습니다.
RequireAtLeastOne<T, Keys extends keyof T>
RequireAtLeastOne 타입은 1)T와 Keys라는 제네릭 타입을 사용해 정의된 타입인데,
그 중 3)Keys라는 타입은 반드시 2)T 타입 객체의 키 값들의 유니언 타입이어야 한다는 뜻 입니다.
조금 헷갈릴 수 있으니 하나씩 breakdown 해서 봅시다.
1. <T, Keys ...>
이는 RequireAtLeastOne이라는 타입은 T와 Keys 두 가지 Generic Type을 사용하여 정의된 제네릭 타입임을 의미합니다.
(제니릭 타입 공식문서)
2. <... keyof T ...>
keyof 연산자는 객체 타입에서 객체의 키 값들을 숫자나 문자열 리터럴 유니언을 생성합니다. (keyof 공식문서)
말이 조금 헷갈릴 수 있으니 예를 들어 봅시다.
type Dog = { species: 'jindo', personality: 'friendly'};
type D = keyof Dog;
위의 예제에서 type D는 'species' | 'personality' 가 될 것입니다.
3. <... Keys extends key of T ...>
제네릭 타입에서 extends는 주로 제약을 걸기 위해 사용 됩니다. (generic contraints 공식문서)
역시 간단한 예를 들어 봅시다.
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a");
getProperty(x, "m");
Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.
Key라는 제네릭 타입은 반드시 Type객체의 키 값들의 유니언 타입이어야 합니다.
즉, "a" | "b" | "c" | "d" 이어야 한다는 뜻입니다.
그래서 만약 getProperty(x, "m");을 호출하면 마지막 줄의 에러가 출력되게 되는 것입니다.
4. RequireAtLeastOne<T, Keys extends key of T>
이제 우리는 아래의 타입을 이해할 수 있습니다.
RequireAtLeastOne<T, Keys extends keyof T>
RequireAtLeastOne 타입은 T와 Keys라는 제네릭 타입을 사용해 정의된 타입인데,
그 중 Keys라는 타입은 반드시 T 타입 객체의 키 값들의 유니언 타입이어야 한다는 뜻입니다.
위에서 해당 타입을 사용할 때 저는 아래처럼 사용 할 수 있다고 언급 했었습니다.
export type ValidDOMElementIdentifierOptions = RequireAtLeastOne<DOMElementIdentifierOptions, 'extraClass' | 'extraId'>;
여기에서 'extraClass' | 'extraId' 가 반드시 DOMElementIdentifierOptions 타입의 키 중 하나여야 한다는 뜻입니다.
Pick<T, Exclude<keyof T, Keys>>
1)T 타입 전체 키 중 Keys가 아닌 타입을 제외한 속성만 2)T 타입에서 뽑아서 새로운 타입을 만듭니다.
말이 조금 어렵지만 하나씩 분리해서 이해해 보도록 하겠습니다.
1. Exclude<UnionType, ExcludedMembers>
Exclude는 UnionType에서 ExcludedMembers를 모두 제외한 타입을 만듭니다. (Exclude 공식문서)
type T1 = Exclude<"a" | "b" | "c", "b">
예를 들어, 위 T1 타입은 "a" | "c" 가 될 것입니다.
그럼 Exclude<keyof T, Keys> 는 T 타입 전체 키에서 인자로 넘긴 Keys를 제외한 타입이 되게 될 것입니다.
2. Pick<Type, Keys>
Pick은 Type에서 Keys 프로퍼티들을 뽑아서 새로운 타입을 만듭니다. (Pick 공식문서)
역시 예를 들어 확인해 봅시다.
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Pick<Todo, "title" | "completed">;
const todo: TodoPreview = {
title: "Clean room",
completed: false,
};
Title 인터페이스에서 "title"과 "completed" 프로퍼티만 뽑아서 새로운 타입, TodoPreview 를 정의했습니다.
{[K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>}[Keys]
위 타입은 Union 타입인 1)Keys에 언급된 속성 중 2)하나 이상은 필수 속성, 그 이외는 Optional 속성이 되는 타입 입니다.
이번에는 조금 복잡 해 보일 수 있지만, 하나씩 분리해서 보면 앞에서 배운 것에서 크게 다르지 않습니다.
1. [K in Keys] -?
Union 타입인 Keys를 순회하면서, K라는 타입을 정의 합니다. (Mapped Type 공식문서)
예를 들어 이해해 봅시다.
type Name = "kim" | "morph" | "mark";
type People = {
[N in Name]: string
}
//People would be like below
const people: People = {
kim: "KIM",
morph: "me",
mark: "MARK"
}
People이라는 타입을 정의할 때, Name 이라는 Union 타입을 순회하면서, kim, morph, mark를 가져오고 이 각각을 string 타입으로 재정의 하였습니다.
참고로, "-?"는 optional 속성을 제거한다는 뜻입니다. (공식문서)
예를 들어, 아래 코드는 Type의 key들을 순회하면서 Property라는 타입을 정의하는데 각 타입은 필수라는 뜻입니다.
혹시나, ?(optional)속성이 있더라도 "-?"로 제거했기 때문입니다.
type Concrete<Type> = {
[Property in keyof Type]-?: Type[Property];
};
2. Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>
Required는 해당 속성을 필수 속성으로 만들어주고, Partial은 Optional 하게 만들어 줍니다. (Required, Partial 공식문서)
& 는 Intersection Type을 생성할 때 사용되는 기호입니다. (Intersection Type 공식문서) 우리가 흔히 아는 AND 연산자(&)라고 생각하시면 편합니다.
따라서 위 타입은, T타입에서 K타입의 키를 가진 속성들을 필수 속성으로 만들고,
T타입에서 Keys 중 K가 아닌 속성들은 옵셔널하게 만든 타입입니다. 말이 조금 헷갈리고 어렵지요?
예를 들어 확인해 봅시다.
type MyType<T, K extends keyof T> = Required<Pick<T, K>> & Partial<Pick<T, Exclude<keyof T, K>>>;
interface Person {
name: string;
age: number;
email: string;
}
// Test case 1: All properties required
const data1: MyType<Person, 'name' | 'age' | 'email'> = {
name: 'John',
age: 30,
email: 'john@example.com',
};
// Test case 2: Some properties required, some optional
const data2: MyType<Person, 'name' | 'age'> = {
name: 'Jane',
age: 25,
};
// Test case 3: email property is optional
const data3: MyType<Person, 'name' | 'age'> = {
name: 'Jane',
age: 25,
email: "home"
};
우선 MyType을 정의 하였습니다.
그리고 Person이라는 인터페이스를 생성 했습니다.
이제 우리가 MyType이라는 타입을 사용 할 때, 두 번째 인자로 주는 K(유니온)타입에 따라 어떻게 타입이 바뀌는지 확인해 봅시다.
다음 링크에서 직접 만져보는 것도 추천 드립니다.(TS Playground)
결국 Person 타입에서, K로 넘겨주는 타입은 필수가 되고, 그 이외의 속성은 Optional이 되게 됩니다.
3.{[K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>}[Keys]
이제 전체를 이어 붙여서 예시와 함께 단계별로 이해해 보겠습니다.
type RequireAtLeastOne = {
[K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>
}[Keys]
interface MenuItem {
title: string;
component?: number;
click?: number;
icon: string;
}
type ClickOrComponent = RequireAtLeastOne<MenuItem, 'click' | 'component'>
위와 같은 상황을 하나씩 생각해 봅시다.
가장 먼저 우린 아래처럼 타입을 정의 했습니다.
RequireAtLeastOne<MenuItem, 'click' | 'component'>
이 타입은 아래로 해석 될 수 있을 것입니다.
{
click: {click: number},
component?: {component: number}
}['click' | 'component']
그리고 결론적으로 아래 타입이 될 것입니다.
{click: number, component?: number} | {click?:number, component: number}
이제 거의 마무리까지 왔지만, 아직은 조금 부족합니다.
우리는 Keys타입으로 언급하지 않은 다른 타입들은 모두 필수적으로 포함되길 원합니다.
앞에서 Pick<T, Exclude<keyof T, Keys>>를 했던 것처럼요.
그럼 두 개의 타입을 Intersection 타입(&)을 이용하여 합쳐 봅시다.
export type RequireAtLeastOne<T, Keys extends keyof T = keyof T> =
Pick<T, Exclude<keyof T, Keys>>
& {
[K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>
}[Keys]
이제 우리가 원하는 대로 RequireAtLeastOne 타입 구현이 완료 되었습니다.
혹시 이해가 안되거나 잘못된 점이 있다면 댓글로 알려주세요!
'TypeScript' 카테고리의 다른 글
제네릭 함수 - 기본부터! (Java && TypeScript) (0) | 2023.03.29 |
---|---|
[TypeScript Error] d3 - tickFormat (0) | 2022.12.12 |
[Solved] 시간 차이 구하기, the left-hand side of an arithmetic operation must be of type 'any' 'number' 'bigint' or an enum type. (0) | 2022.10.27 |
[Solved] Type 'number' is not assignable to type 'bigint' (0) | 2022.08.04 |
"Property '...' does not exist on type 'EventTarget'" in TypeScript (0) | 2022.05.21 |