트리 구조를 위한 유용한 TypeScript 제네릭

발행: (2026년 2월 23일 오후 07:03 GMT+9)
7 분 소요
원문: Dev.to

Source: Dev.to

Cover image for Useful TypeScript generics for tree structures

개발자는 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";
*/

ResultTypeheader.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는 코드를 단순화할 수 있는 필요한 유연성을 가지고 있습니다. 그리고 트리 구조가 꽤 복잡해 보이지만, 제네릭을 사용하면 타입 중복 없이 이를 다룰 수 있습니다. 이 글에서 흥미로운 무언가를 찾으셨길 바랍니다.

프론트엔드 개발을 즐기세요!

0 조회
Back to Blog

관련 글

더 보기 »

Stripe + Next.js 궁극 가이드 (2026년판)

1. 2026 라이프사이클: Embedded vs. Hosted Stripe는 이제 Embedded Checkout을 강력히 추진합니다. 기존 리디렉션과 달리, iframe이나 웹 컴포넌트를 사용하여…

FSCSS 변수 대체 연산자 (||)

FSCSS Variable Fallback Operator의 커버 이미지 ||https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fd...