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

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

[Easy] 4. Pick

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

문제

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

정답

type MyPick<T, K extends keyof T> = { [P in K]: T[P] };

설명

  • T는 원본 타입
  • KT의 프로퍼티 키 타입 (extends keyof T를 활용해 T의 프로퍼티 키 타입으로 제한)
  • [P in K]K의 각 프로퍼티 키를 순회하며 새로운 타입을 생성
  • T[P]P 프로퍼티 키(key)의 값(value)을 가져옴

추가 질문

asextends 차이

  • as: 타입 단언 or 매핑된 타입 변환
  • extends: 타입 제약(generic) or 조건부 타입(conditional type) 정의
// as의 사용(1): 타입 단언
const value: unknown = "hello";
const length = (value as string).length; // 타입 단언

// as의 사용(2): 매핑된 타입 변환
type RenameKeys<T> = {
  [K in keyof T as `new_${string & K}`]: T[K];
};

type Original = { id: number; name: string };
type Renamed = RenameKeys<Original>;
// 결과:
// {
//   new_id: number;
//   new_name: string;
// }
// extends의 사용(1): 타입 제약
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

// extends의 사용(2): 조건부 타입 정의
type IsString<T> = T extends string ? true : false;

type A = IsString<string>; // true
type B = IsString<number>; // false

[P in K]란 무엇인가?

  • []는 매핑된 타입에서 특정 키를 순회하며 새로운 타입을 생성할 때 사용(index 아님)

  • P는 매핑된 키를 나타내는 변수

  • in은 순회(iteration)를 나타내는 키워드로, K의 각 요소를 순회

  • K는 순회할 키의 집합(K는 일반적으로 keyof T처럼 타입의 키 집합)

  • [P in K]는 K에 포함된 키를 하나씩 순회하며 새로운 타입의 키를 정의

type MappedType = {
  [P in K]: ValueType; // 매핑된 타입의 키와 값 정의
};

Reference

[Easy] 7. Readonly

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

문제

T에서 모든 프로퍼티를 읽기 전용으로 만드는 내장 제네릭 Readonly<T>을 사용하지 않고 구현하세요.

정답

type MyReadonly<T> = {
  readonly [P in keyof T]: T[P];
};

설명

  • [P in keyof T]: T의 모든 프로퍼티 키를 순회하며 새로운 타입을 생성
  • readonly: 프로퍼티를 읽기 전용으로 만듦
  • T[P]P 프로퍼티 키(key)의 값(value)을 가져옴

추가 질문

'readonly' 키워드는 무엇인가?

  • Typescript 고유의 Type Modifier
  • 타입 정의 시 특정 프로퍼티를 읽기 전용으로 선언할 때 사용
  • 읽기 전용 프로퍼티는 할당 불가능
  • 객체 타입의 프로퍼티, 배열, 매핑된 타입에 적용 가능
  • 런타임이 아닌 컴파일 타임에만 동작
// 객체 타입의 프로퍼티
interface User {
  readonly id: number;
  name: string;
}

const user: User = { id: 1, name: "Hayou" };
user.id = 2; // 오류: 'id'는 읽기 전용 속성입니다.

// 배열
const numbers: readonly number[] = [1, 2, 3];
numbers[0] = 4; // 오류: 읽기 전용 배열에서는 값을 변경할 수 없습니다.
numbers.push(4); // 오류: 읽기 전용 배열에서는 요소를 추가할 수 없습니다.

// 매핑된 타입
type MyReadonly<T> = {
  readonly [P in keyof T]: T[P];
};

interface User {
  id: number;
  name: string;
}

type ReadonlyUser = MyReadonly<User>;
const user: ReadonlyUser = { id: 1, name: "Hayou" };
user.id = 2; // 오류: 'id'는 읽기 전용 속성입니다.

Reference

[Easy] 11. Tuple to Object

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

문제

배열(튜플)을 받아, 각 원소의 값을 key/value로 갖는 오브젝트 타입을 반환하는 타입을 구현하세요.

정답

type TupleToObject<T extends readonly any[]> = { [P in T[number]]: P };

설명

  • T는 배열(튜플) 타입
  • T[number]는 배열 T의 모든 요소 타입을 나타냄 (배열 T의 인덱스에 있는 모든 타입을 집합으로 반환)
  • [P in T[number]]T[number]의 각 원소를 순회하며 새로운 타입을 생성

추가 질문

[P in T]가 안되는 이유

  • 매핑된 타입에서 [P in ...] 구문은 키(key)를 순회하는 역할을 하며, P는 반드시 "키로 사용할 수 있는 값"이어야 함
  • T['tesla', 'model 3', 'model X']라는 배열의 "전체"를 가리킴
type T = ["tesla", "model 3", "model X"];
  • 매핑된 타입에서 키는 배열 전체가 될 수 없고, 반드시 키로 사용할 수 있는 타입이어야 함

  • 반면, T[number]는 배열 T의 요소 타입만을 추출하여 배열의 모든 요소를 하나의 집합으로 표현

  • 배열 T의 각 요소가 P로 순회될 수 있도록 만들어 줌

type T = ["tesla", "model 3", "model X"];
type TElement = T[number]; // 'tesla' | 'model 3' | 'model X'

Reference

[Easy] 14. First of Array

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

문제

배열(튜플) T를 받아 첫 원소의 타입을 반환하는 제네릭 First<T>를 구현하세요.

정답

type First<T extends any[]> = T extends [] ? never : T[0];

설명

  • T extends []는 배열 T가 비어있는지 확인
  • T[0]는 배열 T의 첫 번째 요소를 가져옴
  • T extends [] ? never : T[0]는 배열 T가 비어있으면 never를 반환하고, 비어있지 않으면 첫 번째 요소를 반환

[Easy] 18. Length of Tuple

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

문제

배열(튜플)을 받아 길이를 반환하는 제네릭 Length<T>를 구현하세요.

정답

type Length<T extends readonly any[]> = T["length"];

설명

  • T extends readonly any[]는 튜플 T가 읽기 전용 배열이라는 것을 확인
  • T["length"]는 튜플 T의 길이를 반환

추가 질문

왜 읽기 전용 배열이어야 하는가?

  • 우선, 예시에서 주어진 튜플은 모두 읽기 전용 배열(as const)이다.
  • 그렇다면 as const와 readonly 모두 제거하면 어떻게 될까?
type Length<T extends any[]> = T["length"]; // readonly 제거

const tesla = ["tesla", "model 3", "model X", "model Y"]; // as const 제거
  • 이 경우 Expect<Equal<Length<typeof tesla>, 4>>Expect<Equal<Length<typeof spaceX>, 5>>가 false가 된다.
  • TypeScript에서 배열 리터럴(const tesla = ['tesla', 'model 3', 'model X', 'model Y'])은 기본적으로 수정 가능한 배열(string[])로 간주됩니다. 즉, typeof tesla의 타입은 string[]이다.
type TeslaType = string[]; // 배열의 요소 타입은 string, 길이는 고정되지 않음
type TeslaTypeAsConst = readonly ["tesla", "model 3", "model X", "model Y"]; // 배열의 요소 타입은 string, 길이는 4
  • Length<T>는 배열 타입에서 length 프로퍼티를 참조한다. 하지만 배열이 단순히 string[]로 추론되면, 배열의 길이에 대한 정보를 포함하지 않음.
  • 따라서 Length<typeof tesla>는 정확히 4라는 값 타입이 아니라, number 타입으로 추론됨

[Easy] 43. Exclude

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

문제

T에서 U에 할당할 수 있는 타입을 제외하는 내장 제네릭 Exclude<T, U>를 이를 사용하지 않고 구현하세요.

정답

type MyExclude<T, U> = T extends U ? never : T;

설명

  • T extends UTU에 할당할 수 있는지(TU의 서브타입인지) 여부를 확인
  • TU에 할당할 수 있으면 never를 반환하고, 그렇지 않으면 T를 반환

추가 질문

유니온 타입('a' | 'b' | 'c')에서 T가 동작하는 방식

  • 조건부 타입의 분배 법칙(Distributive Property)
  • T는 유니온 타입의 각 구성 요소에 대해 개별적으로 판단 (이 예시에서는 extends를 판단)

Reference

profile
세상에 못할 일은 없어!

0개의 댓글