트리 구조를 위한 유용한 TypeScript 제네릭
Source: Dev.to

개발자는 DOM 트리, React 컴포넌트 트리, NPM 의존성 트리와 같은 객체 계층에 둘러싸여 있습니다. 트리 형태의 데이터 구조는 코드에서 꽤 흔하며, 신뢰할 수 있는 TypeScript 타입을 사용하면 이를 다룰 때 더 자신감을 가질 수 있습니다. 이 글에서는 저에게 가장 큰 도움이 된 몇 가지 제네릭을 여러분과 공유하고자 합니다.
Deep partial
때때로 우리는 서드파티 함수의 모든 매개변수, 심지어 중첩된 매개변수까지도 기본값을 제공하고 싶어집니다. 이렇게 하면 모든 매개변수가 선택적이 됩니다. 이 경우 DeepPartial 타입이 도움이 됩니다:
type DeepPartial<T> = T extends object
? {
[P in keyof T]?: DeepPartial<T[P]>;
}
: T;
type InitialType = {
header: {
size: 'sm' | 'md' | 'lg';
color: 'primary' | 'secondary';
nav: {
align: 'left' | 'right';
fontSize: number;
};
};
footer: {
fixed: boolean;
links: {
max: 5 | 10;
nowrap: boolean;
};
};
};
type ResultType = DeepPartial<InitialType>;
/*
type ResultType = {
header?: {
size?: "sm" | "md" | "lg" | undefined;
color?: "primary" | "secondary" | undefined;
nav?: {
align?: "left" | "right" | undefined;
fontSize?: number | undefined;
} | undefined;
} | undefined;
footer?: {
fixed?: boolean | undefined;
links?: {
max?: 5 | 10 | undefined;
nowrap?: boolean | undefined;
} | undefined;
} | undefined;
};
*/
Paths
다른 경우는 사용 가능한 모든 트리 경로를 타입으로 추론해야 할 때입니다. Paths 제네릭을 사용하면 이를 해결할 수 있습니다:
type Paths<T, Exclude extends string = ''> = T extends object
? {
[K in keyof T]: `${Exclude}${'' | Paths<T[K], \`\${Exclude}\${K & string}\`> extends '' ? '' : \`.${Paths<T[K], \`\${Exclude}\${K & string}\`>}\`}`;
}[keyof T]
: never;
type InitialType = {
header: {
size: 'sm' | 'md' | 'lg';
color: 'primary' | 'secondary';
nav: {
align: 'left' | 'right';
fontSize: number;
};
};
footer: {
fixed: boolean;
links: {
max: 5 | 10;
nowrap: boolean;
};
};
};
type ResultType = Paths<InitialType>;
/*
type ResultType =
| "header.size"
| "header.color"
| "header.nav.align"
| "header.nav.fontSize"
| "footer.fixed"
| "footer.links.max"
| "footer.links.nowrap";
*/
Including leaves
때때로 경로에 리프(leaf)까지 포함해야 할 경우가 있습니다. 이때는 다음 제네릭을 사용할 수 있습니다:
type Paths<T, Exclude extends string = ''> = T extends object
? {
[K in keyof T]: `${Exclude}${'' | Paths<T[K], \`\${Exclude}\${K & string}\`> extends '' ? '' : \`.${Paths<T[K], \`\${Exclude}\${K & string}\`>}\`}`;
}[keyof T]
: T extends string | number | boolean
? T
: never;
type InitialType = {
header: {
size: 'sm' | 'md' | 'lg';
color: 'primary' | 'secondary';
nav: {
align: 'left' | 'right';
fontSize: number;
};
};
footer: {
fixed: boolean;
links: {
max: 5 | 10;
nowrap: boolean;
};
};
};
type ResultType = Paths<InitialType>;
/*
type ResultType =
| "header.size.sm"
| "header.size.md"
| "header.size.lg"
| "header.color.primary"
| "header.color.secondary"
| "header.nav.align.left"
| "header.nav.align.right"
| `header.nav.fontSize.${number}`
| "footer.fixed.false"
| "footer.fixed.true"
| "footer.links.max.5"
| "footer.links.max.10"
| "footer.links.nowrap.false"
| "footer.links.nowrap.true";
*/
ResultType에 header.nav.fontSize.${number}와 같은 이상한 요소가 포함된 것을 볼 수 있습니다. 이는 fontSize 매개변수가 무한히 많은 값을 가질 수 있기 때문입니다. 이러한 경로를 제거하도록 제네릭을 수정할 수 있습니다:
type Paths<T, Exclude extends string = ''> = T extends object
? {
[K in keyof T]: `${Exclude}${'' | Paths<T[K], \`\${Exclude}\${K & string}\`> extends '' ? '' : \`.${Paths<T[K], \`\${Exclude}\${K & string}\`>}\`}`;
}[keyof T]
: T extends string | number | boolean
? `${number}` extends `${T}`
? never
: T
: never;
type InitialType = {
header: {
size: 'sm' | 'md' | 'lg';
color: 'primary' | 'secondary';
nav: {
align: 'left' | 'right';
fontSize: number;
};
};
footer: {
caption: string;
fixed: boolean;
links: {
max: 5 | 10;
nowrap: boolean;
};
};
};
type ResultType = Paths<InitialType>;
/*
type ResultType =
| "header.size.sm"
| "header.size.md"
| "header.size.lg"
| "header.color.primary"
| "header.color.secondary"
| "header.nav.fontSize"
| "header.nav.align.left"
| "header.nav.align.right"
| "footer.caption"
| "footer.fixed.false"
| "footer.fixed.true"
| "footer.links.max.5"
| "footer.links.max.10"
| "footer.links.nowrap.false"
| "footer.links.nowrap.true";
*/
노드와 리프
많은 알고리즘에서 트리의 노드와 리프(leaf)를 구분해야 할 필요가 있습니다. 객체 값을 갖는 매개변수를 노드로 간주한다면, 다음과 같은 제네릭을 적용할 수 있습니다:
type Nodes<T, Exclude extends string = ''> = T extends object
? {
[K in keyof T]: T[K] extends object
? `${Exclude}${K & string}` | `${Exclude}${K & string}.${Nodes<T[K], \`\${Exclude}\${K & string}\`>}`
: never;
}[keyof T]
: never;
type Leaves<T, Exclude extends string = ''> = T extends object
? {
[K in keyof T]: `${Exclude}${K & string}${Leaves<T[K], \`\${Exclude}\${K & string}\`> extends never ? '' : \`.${Leaves<T[K], \`\${Exclude}\${K & string}\`>}\`}`;
}[keyof T]
: never;
(원본에서 Leaves 정의는 일부 생략되었습니다.)
TypeScript 예제
type InitialType = {
header: {
size: 'sm' | 'md' | 'lg';
color: 'primary' | 'secondary';
nav: {
align: 'left' | 'right';
fontSize: number;
};
};
footer: {
caption: string;
fixed: boolean;
links: {
max: 5 | 10;
nowrap: boolean;
};
};
};
type ResultNodes = Nodes<InitialType>;
type ResultLeaves = Leaves<InitialType>;
/*
type ResultNodes = "header" | "footer" | "header.nav" | "footer.links";
type ResultLeaves = "header.size" | "header.color" | "header.nav.align" |
"header.nav.fontSize" | "footer.fixed" | "footer.caption" |
"footer.links.max" | "footer.links.nowrap";
*/
전체 화면 제어 (예시)
Enter fullscreen mode
Exit fullscreen mode
Conclusion
보시다시피, TypeScript는 코드를 단순화할 수 있는 필요한 유연성을 가지고 있습니다. 그리고 트리 구조가 꽤 복잡해 보이지만, 제네릭을 사용하면 타입 중복 없이 이를 다룰 수 있습니다. 이 글에서 흥미로운 무언가를 찾으셨길 바랍니다.
프론트엔드 개발을 즐기세요!