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

유한별·2025년 3월 16일
0
post-thumbnail

[Medium] 2757. PartialByKeys

문제

두 개의 타입 인수 TK를 사용하는 PartialByKeys<T, K>를 구성하세요.

K는 옵셔널하며 T의 프로퍼티로 이루어진 유니언 타입을 지정할 수 있습니다. K를 제공하지 않는다면 Partial<T>와 같이 모든 프로퍼티를 옵셔널하게 만들어야 합니다.

예시

interface User {
  name: string;
  age: number;
  address: string;
}

type UserPartialName = PartialByKeys<User, "name">; // { name?:string; age:number; address:string }

시도 1 (정답)

접근 방식

  • 키 값을 순회하면서, 조건부 처리를 해보자

코드

type PartialByKeys<
  T,
  K extends keyof T = keyof T,
  U = {
    [Key in keyof T as Key extends K ? Key : never]?: T[Key];
  } & Omit<T, K>,
> = { [UK in keyof U]: U[UK] };

코드 설명

  • 우선 Tkey 값을 순회하기 위해 Key in keyof T 를 사용
  • 이 때, K 타입에 해당하는 키 값만 옵셔널하게 처리하기 위해 Key extends K 조건을 추가
  • K 타입에 해당하는 키 값만 옵셔널하게 처리
  • 이렇게 만들어진 타입을 U라고 하자
  • K 타입에 해당하지 않는 키 값들은 Omit<T, K> 타입으로 만들어 두 타입을 인터섹션 처리
  • 그렇게 만들어진 인터섹션 타입 U을 순회하여 객체로 반환

[Medium] 2759. Required By Keys

문제

두 개의 타입 인자 TK를 받는 제네릭 타입 RequiredByKeys<T, K>를 구현하세요.

K는 필수(required)로 설정되어야 하는 T의 속성들을 지정합니다. K가 제공되지 않을 경우, 일반적인 Required<T>처럼 모든 속성을 필수로 만들어야 합니다.

예시

interface User {
  name?: string;
  age?: number;
  address?: string;
}

type UserRequiredName = RequiredByKeys<User, "name">; // { name: string; age?: number; address?: string }

시도 1 (정답)

접근 방식

  • 옵셔널한 키 값들을 필수로 만들기 위해 Required를 사용
  • Omit & Pick을 활용해 한번에 처리

코드

type RequiredByKeys<T, K extends keyof T = keyof T> = Omit<T, K> &
  Required<Pick<T, K>> extends infer O
  ? { [Key in keyof O]: O[Key] }
  : never;

코드 설명

  • Omit<T, K>: T에서 K 키 값들을 제외한 타입
  • Required<Pick<T, K>>: T에서 K 키 값들만 선택하고, 그 값들을 필수로 만든 타입
  • extends infer O: 위 두 타입을 인터섹션 처리하여 하나의 타입으로 만듦
  • ? { [Key in keyof O]: O[Key] }: 위 타입을 객체로 반환

[Medium] 2793. Mutable

문제

제네릭 Mutable<T>를 구현하세요. 이는 T의 모든 속성을 가변(mutable)하게 만듭니다 (readonly가 아니게).

예시:

interface Todo {
  readonly title: string;
  readonly description: string;
  readonly completed: boolean;
}

type MutableTodo = Mutable<Todo>; // { title: string; description: string; completed: boolean; }

시도 1

접근 방식

  • 키 순회 및 리맵핑 과정에서 조건부 처리로 해볼 수 있지 않을까?
  • 구현 불가능

시도 2

접근 방식

  • 아예 키값들을 따로 떼오고, 그걸 순회하며 뭔가 처리해보자
  • -readonly라는 키워드가 있네

코드

type Mutable<T, K extends keyof T = keyof T> = {
  -readonly [P in keyof T as P extends K ? P : never]: T[P];
};

실패 이유

  • 리스트 타입에 대해서 처리 불가능

시도 3

접근 방식

  • 리스트 먼저 처리 후 나머지 타입에 대해 처리

코드

type Mutable<T> = T extends readonly [...infer R]
  ? [...R]
  : { -readonly [P in keyof T]: T[P] };

실패 이유

  • 타입 에러 처리 불가능

시도 4 (정답)

접근 방식

  • 리스트나 객체가 아닌 값에 대해 에러 처리를 해줘야하기 때문에, object 타입만 받도록 설정

코드

type Mutable<T extends object> = T extends readonly [...infer R]
  ? [...R]
  : { -readonly [P in keyof T]: T[P] };

코드 설명

  • 만약 Treadonly 리스트 타입이라면, 해당 원소들을 스프레드 연산자로 풀어서 반환
  • 만약 Treadonly 객체 타입이라면, 해당 속성들을 순회하며 -readonly 키워드를 붙여서 반환

[Medium] 2852. Omit By Type

문제

T에서 U 타입에 할당할 수 없는 속성들의 집합을 선택하세요.

예시

type OmitBoolean = OmitByType<
  {
    name: string;
    count: number;
    isReadonly: boolean;
    isEnable: boolean;
  },
  boolean
>; // { name: string; count: number }

시도 1 (정답)

접근 방식

  • 키 값 순회하면서 키 리맵핑으로 조건부 처리

코드

type OmitByType<T, U> = { [K in keyof T as T[K] extends U ? never : K]: T[K] };

코드 설명

  • T[K] extends U ? never : K를 통해 값 타입이 U에 할당 가능한지 확인
  • T[K] 타입이 U 타입에 할당할 수 없는 경우 never 처리(키-값 쌍 제거)

[Medium] 2946. Object Entries

문제

Object.entries 함수의 타입 버전을 구현하세요.

예시

interface Model {
  name: string;
  age: number;
  locations: string[] | null;
}
type modelEntries = ObjectEntries<Model>; // ['name', string] | ['age', number] | ['locations', string[] | null];

시도 1

접근 방식

  • 키를 순회하여 키-값 쌍을 tuple로 만들어 해당 tuple들의 tuple을 만들자
  • 그렇게 만들어진 tuple들을 union으로 변경하여 반환

코드

type ObjectEntries<T> = {
  [K in keyof T]: [K, T[K] extends undefined ? undefined : T[K]];
}[keyof T];

실패 이유

  • 옵셔널한 값들에 대해서 처리 불가능

시도 2

접근 방식

  • 같은 방식으로, 옵셔널한 값들을 처리할 방법을 생각해보자.
  • -? 키워드 참고
type Required<T> = {
  [K in keyof T]-?: T[K];
};

참고

코드

type ObjectEntries<T> = {
  [K in keyof T]-?: [K, T[K] extends undefined ? undefined : T[K]];
}[keyof T];

실패 이유

  • partial에 대해서 처리가 안됨

시도 3

접근 방식

  • any | undefined 타입에 대해서 처리 해버리자

코드

type ObjectEntries<T> = {
  [K in keyof T]-?: [
    K,
    T[K] extends undefined ? undefined : Exclude<T[K], undefined>,
  ];
}[keyof T];

실패 이유

  • K가 옵셔널일 때만 처리해야되는데, 마지막 예시와 같이 필수 요소의 any | undefined도 처리해버림
  • Equal<ObjectEntries<{ key: string | undefined }>, ["key", string | undefined]> 이 테스트 케이스 실패

시도 4 (정답)

접근 방식

  • 키 값이 옵셔널일 때 한 번 더 분기 처리 해보자.

코드

type IsOptional<T, K extends keyof T> = {} extends Pick<T, K> ? true : false;

type ObjectEntries<T> = {
  [K in keyof T]-?: IsOptional<T, K> extends true
    ? [K, T[K] extends undefined ? undefined : Exclude<T[K], undefined>]
    : [K, T[K] extends undefined ? undefined : T[K]];
}[keyof T];

코드 설명

  • {} extends Pick<T, K>의 동작 원리: Pick<T, K>는 T의 키 중 K를 선택하여 반환하는 타입이다.
type Example = {
  required: number;
  optional?: string;
};

type PickRequired = Pick<Example, "required">;
// 결과: { required: number }

type IsOptional1 = {} extends PickRequired ? true : false;
// 결과: false

type PickedOptional = Pick<Example, "optional">;
// 결과: { optional?: string }

type IsOptional2 = {} extends PickedOptional ? true : false;
// 결과: true
  • {} extends PickedOptional 조건은 빈 객체가 PickedOptional의 서브타입인지 확인한다.

  • 만약 required 키가 있는 경우, {}PickedOptional의 서브타입이 아니므로 조건이 거짓이 된다.

  • 따라서, {} extends Pick<T, K>K가 옵셔널한 키인 경우에만 참이 된다.

  • T[K] extends undefined ? undefined : ... 이 부분은 값이 undefined인 경우에는 undefined를 반환하고, 그렇지 않은 경우에는 처리된 값을 반환한다.

[Medium] 3062. Shift

문제

제네릭 Shift<T>를 구현하세요. 이는 T의 첫 번째 요소를 제거한 배열을 반환합니다.

예시

type Result = Shift<[3, 2, 1]>; // [2, 1]

시도 1 (정답)

접근 방식

  • 빈 배열 처리 후, infer를 통해 맨 앞의 요소를 날릴 수 있지 않을까?

코드

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

코드 설명

  • 최초 T extends [] 처리로 빈 배열 처리
  • infer를 통해 맨 앞의 요소를 선택해 나머지 배열을 반환
profile
세상에 못할 일은 없어!

0개의 댓글