[TS] Type-Challenges 스터디 10주차

유한별·2025년 3월 16일
0

타입 챌린지 스터디

목록 보기
10/19
post-thumbnail

[Medium] 3188. Tuple to Nested Object

문제

Given a tuple type T that only contains string type, and a type U, build an object recursively.

주어진 튜플 타입 T 가 문자열 타입만 포함하고 있고, 타입 U 가 주어졌을 때, 재귀적으로 객체를 만드세요.

예시

type a = TupleToNestedObject<["a"], string>; // {a: string}
type b = TupleToNestedObject<["a", "b"], number>; // {a: {b: number}}
type c = TupleToNestedObject<[], boolean>; // boolean. if the tuple is empty, just return the U type

시도 1

접근 방식

  • 먼저 빈 배열 처리
  • 재귀로 튜플의 First와 Rest를 나눠서 First: {Rest: U} 이런 형태가 되도록 처리

코드

type TupleToNestedObject<T extends any[], U> = T extends []
  ? U
  : T extends [infer First, ...infer Rest]
    ? {First: TupleToNestedObject<Rest, U>}
    : never;
]

실패 이유

  • {First: TupleToNestedObject<Rest, U>}에서 사용된 Firstinfer로 추론된 First 값이 아닌 문자열 'First'로 추론됨

시도 2 (정답)

접근 방식

  • 문제 조건에 따라 string으로 키값 제어, K라는 변수로 First를 키값으로 받게 추가

코드

type TupleToNestedObject<T extends string[], U> = T extends [
  infer First extends string,
  ...infer Rest extends string[]
]
  ? { [K in First]: TupleToNestedObject<Rest, U> }
  : U;

코드 설명

  • infer First extends string, ...infer Rest extends string[] 형태로 튜플의 First와 Rest를 나눠서 재귀적으로 처리
  • [K in First]: TupleToNestedObject<Rest, U> 형태로 키값을 First로 받고, 나머지 부분을 재귀적으로 처리
  • 빈 튜플일 경우, U 타입을 그대로 반환

[Medium] 3192. Reverse

문제

튜플을 뒤집는 타입을 구현하세요.

예시

type a = Reverse<["a", "b"]>; // ['b', 'a']
type b = Reverse<["a", "b", "c"]>; // ['c', 'b', 'a']

시도 1 (정답)

접근 방식

  • 재귀로 튜플의 First와 Rest를 나눠서 First를 맨 뒤로 보내는 방식

코드

type Reverse<T extends any[]> = T extends [infer First, ...infer Rest]
  ? [...Reverse<Rest>, First]
  : T;

코드 설명

  • T extends any[] 형태로 튜플 타입 제어
  • T extends [infer First, ...infer Rest] 형태로 튜플의 First와 Rest를 나눠서 재귀적으로 처리
  • [...Reverse<Rest>, First] 형태로 Rest를 뒤집은 결과(재귀)에 First를 추가
  • 빈 튜플일 경우, T 타입을 그대로 반환

[Medium] 3196. FlipArguments

문제

lodash의 _.flip 함수를 타입으로 구현하세요.

FlipArguments<T> 타입은 함수 타입 T를 요구하며, 동일한 반환 타입을 가지지만 매개변수가 반대로 된 새로운 함수 타입을 반환합니다.

예시

type Flipped = FlipArguments<
  (arg0: string, arg1: number, arg2: boolean) => void
>;
// (arg0: boolean, arg1: number, arg2: string) => void

시도 1 (정답)

접근 방식

  • Targs를 순회하며 각각의 arg type을 저장한 뒤, reverse 해서 다시 할당해보자.
  • 3192-reverse 문제를 참고

코드

type Reverse<T extends any[]> = T extends [infer First, ...infer Rest]
  ? [...Reverse<Rest>, First]
  : T;

type FlipArguments<T extends (...args: any[]) => any> = T extends (
  ...args: infer Args
) => infer ReturnType
  ? (...args: Reverse<Args>) => ReturnType
  : never;

코드 설명

  • T extends (...args: any[]) => any 형태로 함수 타입 제어
  • ...args: infer Args 형태로 함수의 인자 타입을 추론 (인자는 튜플 타입으로 추론됨)
  • (...args: Reverse<Args>) => ReturnType 형태로 인자를 뒤집은 뒤, 반환 타입을 그대로 반환
  • 함수 타입이 아닌 경우, never 타입 반환

[Medium] 3243. FlattenDepth

문제

배열을 주어진 깊이까지 재귀적으로 평탄화하는 타입을 구현하세요.

예시

type a = FlattenDepth<[1, 2, [3, 4], [[[5]]]], 2>; // [1, 2, 3, 4, [5]]. flattern 2 times
type b = FlattenDepth<[1, 2, [3, 4], [[[5]]]]>; // [1, 2, 3, 4, [[5]]]. Depth defaults to be 1

시도 1 (정답)

접근 방식

  • 기존 코드 활용(2257-minus-one, 459-flatten)
  • N이 0이 될 때까지 flatten 함수를 재귀적으로 호출
  • flatten은 459-flatten과 다르게 1회만 실행하도록 처리

코드

/* _____________ 기존 코드 활용 _____________ */

// 한 자리 수의 마이너스 1 처리용 타입
type CalcMinusOne = {
  [key: string]: string;
  "1": "0";
  "2": "1";
  "3": "2";
  "4": "3";
  "5": "4";
  "6": "5";
  "7": "6";
  "8": "7";
  "9": "8";
  "0": "9";
};

// 문자열 배열(튜플)의 마지막 요소 추출
type Last<T extends string[]> = T extends [...infer _, infer Last]
  ? Last
  : never;

// 문자열을 튜플로 변환
type StringToTuple<S extends String> = S extends `${infer First}${infer Last}`
  ? [First, ...StringToTuple<Last>]
  : [];

// 튜플을 문자열로 변환
type Join<T extends string[]> = T extends [
  infer First extends string,
  ...infer Rest extends string[]
]
  ? `${First}${Join<Rest>}`
  : "";

// 마지막 요소를 제거한 튜플을 문자열로 변환
type RemoveLast<T extends string[]> = T extends [
  ...infer Rest extends string[],
  infer _
]
  ? Join<Rest>
  : "";

// 문자열을 숫자로 변환
type StringToNumber<S extends string> = S extends `${infer N extends number}`
  ? N
  : never;

// 문자열 배열(튜플)의 마지막 요소가 0인 경우, 마지막 요소를 제거한 문자열 배열(튜플)을 문자열로 변환
// 만약 마지막 요소가 0이면 9로 변환, 그리고 앞 요소에 대해서도 재귀 돌리기
type MinusOneInStringArray<
  S extends string,
  ST extends string[] = StringToTuple<S>
> = Last<ST> extends "0"
  ? S extends "10"
    ? "9"
    : `${MinusOneInStringArray<RemoveLast<ST>>}9`
  : Last<ST> extends "_"
  ? `${MinusOneInStringArray<RemoveLast<ST>>}_`
  : `${RemoveLast<ST>}${CalcMinusOne[Last<ST>]}`;

// 재귀 돌리기 전처리
type MinusOne<
  T extends number,
  S extends string[] = StringToTuple<`${T}`>
> = Last<S> extends "0"
  ? StringToNumber<`${MinusOneInStringArray<RemoveLast<S>>}9`>
  : StringToNumber<`${RemoveLast<S>}${CalcMinusOne[Last<S>]}`>;

// Flatten 함수(1회만 실행하는)
type Flatten<T extends any[]> = T extends [infer First, ...infer Rest]
  ? First extends any[]
    ? [...First, ...Flatten<Rest>]
    : [First, ...Flatten<Rest>]
  : [];

/* _____________ Your Code Here _____________ */

type FlattenDepth<T extends any[], N extends number = 1> = N extends 0
  ? T
  : FlattenDepth<Flatten<T>, MinusOne<N>>;

실패 이유

  • 재귀 호출 횟수가 너무 많아서 런타임 오류 발생
  • 재귀 호출 횟수를 제한하는 방법을 찾아야 함

시도 2 (정답)

접근 방식

  • 만약 이미 flatten된 배열이면 depth가 남았어도 재귀 끝내기

코드

type FlattenDepth<T extends any[], N extends number = 1> = N extends 0
  ? T
  : T extends (string | number | symbol)[]
  ? T
  : FlattenDepth<Flatten<T>, MinusOne<N>>;

코드 설명

  • N extends 0 형태로 N이 0이 될 때까지 재귀 호출
  • T extends (string | number | symbol)[] 형태로 이미 flatten된 배열인 경우 재귀 끝내기
  • FlattenDepth<Flatten<T>, MinusOne<N>> 형태로 flatten 함수를 재귀적으로 호출

더 나은 방법

  • 빈 배열을 하나 생성 후, 재귀 호출할 때마다 배열에 요소를 추가해 1개씩 배열의 길이 늘리기
  • 이후 주어진 N과 배열의 길이(CountArray['length'])를 비교하여 재귀 호출 중단

[Medium] 3326. BEM-style-string

문제

Block, Element, Modifier 방법론(BEM)은 CSS에서 클래스 명명에 널리 사용되는 규칙입니다.

예를 들어, 블록 컴포넌트는 btn으로 표현되고, 블록에 종속된 엘리먼트는 btn__price로 표현되며, 블록의 스타일을 변경하는 수정자는 btn--big 또는 btn__price--warning으로 표현됩니다.

BEM<B, E, M>을 구현하세요. 이는 세 개의 매개변수로부터 문자열 유니온을 생성합니다. 여기서 B는 문자열 리터럴이고, EM은 문자열 배열입니다(비어있을 수 있음).

예시

type a = BEM<"btn", ["price"], []>; // 'btn__price'

시도 1 (정답)

접근 방식

  • 주어진 E, M을 각각 순회하면서 합쳐서 보여줄 방법을 찾자.
  • 문자열 관련된 문제이니, string 리터럴 타입을 고려해보자.

코드

type BEM<
  B extends string,
  E extends string[],
  M extends string[]
> = `${B}${E extends [] ? "" : `__${E[number]}`}${M extends []
  ? ""
  : `--${M[number]}`}`;

코드 설명

  • E extends [] ? "" : __${E[number]}`` 형태로 E가 빈 배열인 경우 빈 문자열 반환, 비어있지 않을 경우 순회하여 유니온 요소 각각 생성
  • 마찬가지로 M 또한 동일한 로직으로 작동

[Medium] 3376. InorderTraversal

문제

이진 트리의 중위 순회를 구현하는 타입을 작성하세요.

예시

const tree1 = {
  val: 1,
  left: null,
  right: {
    val: 2,
    left: {
      val: 3,
      left: null,
      right: null,
    },
    right: null,
  },
} as const;

type A = InorderTraversal<typeof tree1>; // [1, 3, 2]

시도 1 (정답)

접근 방식

  • 재귀를 통해 중위 순회를 구현하자.
  • 중위 순회는 왼쪽, 밸류값, 오른쪽 순서로 순회.

코드

interface TreeNode {
  val: number;
  left: TreeNode | null;
  right: TreeNode | null;
}

type InorderTraversal<T extends TreeNode | null> = T extends TreeNode
  ? [...InorderTraversal<T["left"]>, T["val"], ...InorderTraversal<T["right"]>]
  : [];

코드 설명

  • T extends TreeNode 형태로 TreeNode 타입인 경우 재귀 호출
  • T extends TreeNode | null 형태로 TreeNode 타입이 아닌 경우 빈 배열 반환
  • [...InorderTraversal<T["left"]>, T["val"], ...InorderTraversal<T["right"]>] 형태로 왼쪽, 밸류값, 오른쪽 순서로 순회
profile
세상에 못할 일은 없어!

0개의 댓글