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

유한별·2025년 1월 25일
post-thumbnail

[Medium] 2. Return Type

View on GitHub: https://tsch.js.org/2

문제

내장 제네릭 ReturnType<T>을 이를 사용하지 않고 구현하세요.

정답

type MyReturnType<T extends Function> = T extends (...args: any) => infer U
  ? U
  : never;

설명

  • ReturnType<T>이란 T의 반환 타입을 반환하는 제네릭 타입이다.
  • 우선, TFunction 타입인지 확인
  • 맞다면 반환 타입을 infer U로 추론해 추론한 타입 U를 반환
  • 함수가 아니라면 never 반환
  • parameters와 마찬가지로 ...args는 아무 이름이어도 되고, TypeScript에서 그냥 ‘추론할 위치’가 rest parameter임을 표시하기 위한 placeholder

추가 질문

`(...args:any)는 되고 (args: any)는 안되는 이유는?

  • (...args: any) 형태는 0개 이상의 매개변수를 허용하며, 모든 매개변수는 배열 형태로 캡처
  • (args: any) 형태는 1개의 매개변수를 허용하며, 매개변수는 단일 값으로 캡처
  • 따라서 f1와 같이 2개 이상의 매개변수를 받기 위해서는 (...args: any) 형태로 작성해야 함

Reference

[Medium] 3. Omit

View on GitHub: https://tsch.js.org/3

문제

T에서 K 프로퍼티만 제거해 새로운 오브젝트 타입을 만드는 내장 제네릭 Omit<T, K>를 이를 사용하지 않고 구현하세요.

정답

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

설명

  • 우선 K extends keyof T를 통해 KT의 키 값임을 명시
  • P in keyof T를 통해 T의 키 값들을 순회
  • as를 통해 PK에 포함되지 않는 경우에만 P를 반환
  • never는 타입 시스템에서 제거 되기 때문에, PK에 포함되지 않는 경우에만 P: T[P]가 반환 됨

추가 질문

as 딥 다이브

  • as를 활용한 키 리맵핑은 타입스크립트 4.1 버전에 추가된 기능
  • 매핑된 타입에 as 절을 사용해서 매핑된 타입의 키를 다시 매핑할 수 있음
type MappedTypeWithNewProperties<Type> = {
  [Properties in keyof Type as NewKeyType]: Type[Properties];
};
  • 템플릿 리터럴 타입과 같은 기능을 활용해서 이전 프로퍼티에서 새로운 프로퍼티 이름을 만들 수 있음
type Getters<Type> = {
  [Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property];
};

interface Person {
  name: string;
  age: number;
  location: string;
}

type LazyPerson = Getters<Person>;
// type LazyPerson = {
//     getName: () => string;
//     getAge: () => number;
//     getLocation: () => string;
// }
  • 정답에서 사용했듯이 조건부 타입을 통해 never를 생성해서 키를 필터링할 수 있음
  • string | number | symbol 의 조합뿐만 아니라 모든 타입의 조합을 임의로 매핑할 수 있음

Reference

[Medium] 8. Readonly 2

View on GitHub: https://tsch.js.org/8

문제

T에서 K 프로퍼티만 읽기 전용으로 설정해 새로운 오브젝트 타입을 만드는 제네릭 MyReadonly2<T, K>를 구현하세요. K가 주어지지 않으면 단순히 Readonly<T>처럼 모든 프로퍼티를 읽기 전용으로 설정해야 합니다.

정답

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

설명

  • 3-omit 문제에서 사용한 as를 활용한 필터링 기능 사용
  • 첫 번째 집합에는 K에 할당할 수 있는 T의 키 값인 P에 대해서는 readonly 키워드를 붙이고, 그렇지 않은 키 값에 대해서는 제거
  • 두 번째 집합에는 K에 할당할 수 있는 T의 키 값인 P에 대해서는 제거, 그렇지 않은 키 값에 대해서는 기존 타입 유지
  • 두 집합을 intersection 타입으로 합치면 원하는 타입이 완성

추가 질문

왜 두 집합을 union 타입으로 합치면 안되는걸까?

type MyReadonly2<T, K extends keyof T = keyof T> =
  | {
      readonly [P in keyof T as P extends K ? P : never]: T[P];
    }
  | { [P in keyof T as P extends K ? never : P]: T[P] };
  • union 타입으로 합칠 경우 readonly 프로퍼티를 가진 객체와 그렇지 않은 객체로 분리됨
type Example = { foo: string; bar: number; baz: boolean };

type Result = MyReadonly2<Example, "foo" | "bar">;
// Result = { readonly foo: string; readonly bar: number } | { baz: boolean };

Intersection은 교집합인데, 왜 서로 다른 타입을 합치는 것처럼 보이는걸까?

  • 첫 번째 집합과 두 번째 집합은 key가 서로 다른 객체이다.

  • 그렇다면 intersection(교차) 타입은 두 집합의 교집합(공집합)을 반환해야 하는 것이 아닌가?

  • TypeScript에서 교차 타입은 교집합이 아니라 두 타입을 "결합"한 새로운 타입을 반환

  • 두 개 이상의 타입을 조합하여 하나의 타입을 만들어 각 타입의 속성을 모두 포함하는 타입이 생성됨

type A = { foo: string };
type B = { bar: number };
type C = A & B;

// 결과 타입: { foo: string; bar: number }
  • 교차 타입의 결합 방식 1) 두 타입에 동일한 키가 있는 경우
type A = { foo: string };
type B = { foo: string };
type C = A & B;
// 결과: { foo: string }
  • 교차 타입의 결합 방식 2) 두 타입에 동일한 키가 있지만 값의 타입이 다른 경우
type A = { foo: string };
type B = { foo: number };
type C = A & B;
// 결과: { foo: never }
  • 교차 타입의 결합 방식 3) 서로 다른 키를 가지는 경우
type A = { foo: string };
type B = { bar: number };
type C = A & B;
// 결과: { foo: string; bar: number }
  • 교차 타입의 결합 방식 4) 함수 타입의 경우(모든 시그니처를 동시에 만족해야 함)
// 예시1
type A = (x: number) => string;
type B = (x: number) => number;
type C = A & B;

// C는 두 시그니처를 모두 만족해야 함
const fn: C = (x: number) => {
  if (x > 0) return "string";
  return 123; // 타입 에러 (반환 타입이 모두 만족하지 않음)
};

// 예시2
type A = (x: string) => number;
type B = (x: number) => string;

type C = A & B;

const fn: C = (x: any) => {
  if (typeof x === "string") return x.length; // x가 string일 때 number 반환
  if (typeof x === "number") return x.toString(); // x가 number일 때 string 반환
};

Reference

[Medium] 9. Deep Readonly

View on GitHub: https://tsch.js.org/9

문제

객체의 프로퍼티와 모든 하위 객체를 재귀적으로 읽기 전용으로 설정하는 제네릭 DeepReadonly<T>를 구현하세요.

이 챌린지에서는 타입 파라미터 T를 객체 타입으로 제한하고 있습니다. 객체뿐만 아니라 배열, 함수, 클래스 등 가능한 다양한 형태의 타입 파라미터를 사용하도록 도전해 보세요.

정답

type DeepReadonly<T> = T extends object
  ? T extends Function
    ? T
    : { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : T;

설명

  • 첫 번째 조건부 타입은 T가 객체인 경우를 체크, 객체가 아닌 경우 T를 반환
  • 두 번째 조건부 타입은 T가 객체이지만 함수일 경우 T를 반환 (해당 조건이 없을 경우, 함수일 경우 빈 객체를 반환)
  • 만약 T가 객체이고 함수가 아니라면 객체의 프로퍼티를 읽기 전용으로 설정한 타입을 반환

추가 질문

배열이 제대로 처리되는 이유

  • 우선, 배열은 객체이자 함수가 아님.
  • 따라서 T가 배열일 경우 { readonly [K in keyof T]: DeepReadonly<T[K]> } 타입을 반환
  • 배열에서 keyof는 인덱스와 특수키(length, push 등)를 포함함.
type TestArray = [1, 2, 3];
type Keys = keyof TestArray; // "0" | "1" | "2" | "length" | "push" | "pop" | "concat" | ...
  • 인덱스를 통해 배열 요소에 접근, 각 요소에 대해 DeepReadonly를 재귀적으로 호출
  • 특수 키의 경우 이미 읽기 전용으로 설정되어 있어 별도로 표시하지 않음

Reference

[Medium] 10. Tuple to Union

View on GitHub: https://tsch.js.org/10

문제

튜플 값으로 유니온 타입을 생성하는 제네릭 TupleToUnion<T>를 구현하세요.

정답

type TupleToUnion<T extends readonly any[]> = T[number];

해설

  • 11-tuple-to-object에서 사용한 T[number]을 활용해 union 타입 반환

추가 질문

Reference

[Medium] 12. Chainable Options

View on GitHub: https://tsch.js.org/12

문제

체인 가능 옵션은 일반적으로 Javascript에서 사용됩니다. 하지만 TypeScript로 전환하면 제대로 구현할 수 있나요?

이 챌린지에서는 option(key, value)get() 두가지 함수를 제공하는 객체(또는 클래스) 타입을 구현해야 합니다. 현재 타입을 option으로 지정된 키와 값으로 확장할 수 있고 get으로 최종 결과를 가져올 수 있어야 합니다.

예시

declare const config Chainable;

const result = config
  .option("foo", 123)
  .option("name", "type-challenges")
  .option("bar", { value: "Hello World" })
  .get();

// 결과는 다음과 같습니다:
interface Result {
  foo: number;
  name: string;
  bar: {
    value: string;
  };
}

문제를 해결하기 위해 js/ts 로직을 작성할 필요는 없습니다. 단지 타입 수준입니다.

keystring만 허용하고 value는 무엇이든 될 수 있다고 가정합니다. 같은 key는 두 번 전달되지 않습니다.

풀이

type Chainable<CurrentConfig = object> = {
  option<OptionKey extends string, OptionValue extends any>(
    key: Exclude<OptionKey, keyof CurrentConfig>,
    value: OptionValue
  ): Chainable<Omit<CurrentConfig, OptionKey> & Record<OptionKey, OptionValue>>;
  get(): CurrentConfig;
};
  • CurrentConfig를 통해 현재까지 설정된 객체를 추적
  • CurrentConfig는 최초 타입 선언 시 빈 객체로 초기화
  • option 함수는 새로운 key-value 쌍을 추가
  • 이 때 OptionKeystring, OptionValueany로 선언
  • Exclude<OptionKey, keyof CurrentConfig>을 통해 이미 존재하는 key를 제외
  • Omit<CurrentConfig, OptionKey>을 통해 기존 객체에서 해당 key를 제거
  • Record<OptionKey, OptionValue>을 통해 새로운 key-value 쌍을 추가
  • Chainable 타입은 OmitRecord를 통해 중복되지 않은 key를 가진 새로운 객체를 반환
  • get 함수는 최종 결과를 반환

추가 질문

Reference

profile
세상에 못할 일은 없어!

0개의 댓글