type challenges 13번, 4번, 7번, 11번

😃 type challenges를 시작한 이유


이전에 팀 프로젝트를 하면서 TypeScript에 입문한 이후로 잘은 사용하지 못하더라도

사용하면서 장점이 많다고 느꼈고 또 사용하다보니 또 적응이 되어서 그런지 계속 사용하고 있었다.

하지만 조금 더 잘 사용하고 싶다고 생각을 하면서 서적이나 자료들을 찾아보기는 했지만

실제로 사용할 부분을 찾기가 힘들었다.

그래서 한 번 평소 하던 코딩테스트 처럼 직접 사용하면서 학습할 수 있는 것이 없는가 해서

type challenges를 찾게 되었고 앞으로 틈날 때 마다 문제 풀이를 시도할 것 같다.


📕 13번 Hello World


문제 링크: https://github.com/type-challenges/type-challenges/blob/main/questions/00013-warm-hello-world/README.ko.md

아주 기본적인 문제로 문제는 위 링크를 통해서 확인할 수 있다.

문제 풀이도 아주 단순하게 HelloWorld라는 type을 string으로 설정해주면 된다.

말 그대로 warm-up 단계 문제라 가볍게 TypeScript를 사용해보았는가를 확인하는 문제이다.


📝 4번, 7번, 11번을 풀기 앞서서


이제 easy 난이도 문제를 들어가는데, 풀기 전에 꼭 알아야 할 개념이 존재한다.

본인도 이 개념을 푸는 와중에 알게 되었는데 바로 매핑된 타입(mapped type) 이다.

매핑된 타입(mapped type)

TypeScript는 다른 타임의 속성을 기반으로 새로운 타입을 생성하는 구문을 제공한다.

즉, 하나의 타입에서 다른 타입으로 매핑한다는 것이다.

매핑된 타입은 다른 타입을 가져와서 해당 타입의 각 속성에 대해 일부 작업을 수행하는 타입을 말한다.

매핑된 타입은 키 집합의 각 키에 대한 새로운 속성을 만들어 새로운 타입을 생성한다.

매핑된 타입은 [K in OriginalType] 과 같이 in을 사용해 다른 타입으로부터 계산된 타입을 생성한다.

기본 문법

type NewType = {
  [K in OriginalType]: NewProperty;
};

// 예를 들면 아래와 같이 사용할 수 있다.
type Animals = 'alligator' | 'baboon' | 'cat';

type AnimalCounts = {
  [K in Animals]: number;
};

// {
//     alligator: number;
//     baboon: number;
//     cat: number;
// }

위와 같이 사용을 할 수 있다.

기본적인 문법만 살펴보면 실제로 어떻게 사용하는지 모르기 때문에 다른 예시들도 확인해본다.

타입에서 매핑된 타입

interface AnimalVariants {
  alligator: boolean;
  baboon: number;
  cat: string;
}

type AnimalCounts = {
  [K in keyof AnimalVariants]: number;
};

// {
//     alligator: number;
//     baboon: number;
//     cat: number;
// }

위와 같이 사용을 할 수 있다.

일반적으로 매핑된 타입은 존재하는 타입에 keyof 연산자를 사용해 키를 가져오는 방식으로 작동한다.

제네릭 매핑된 타입

type MakeReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

interface Species {
  genus: string;
  name: string;
}

type ReadonlySpecies = MakeReadonly<Species>;

// {
//    readonly genus: string;
//    readonly name: string;
// }

매핑된 타입의 장점중 하나는 제네릭과 결합해 단일 타입 매핑을 다른 타입에서 재사용할 때 나타난다.

매핑된 타입은 매핑된 타입 자체의 타입 매개변수를 포함해 keyof로 해당 스코프에 있는 모든 타입 이름에 접근할 수 있다.

위의 예시는 MakeReadonly 제네릭 타입으로 모든 타입을 사용할 수 있고, 모든 멤버에 readonly 제한자가 추가도니 새로운 버전을 만드는 예시이다.


📕 4번 Pick


문제 링크: https://github.com/type-challenges/type-challenges/blob/main/questions/00004-easy-pick/README.ko.md

이 문제의 경우 유틸리티 타입 중 하나인 Pick을 직접 구현해보는 문제이다.

우선 Pick 이란 것이 생소하니 Pick에 대해서 알아본다.

Pick

Pick의 경우 특정 타입에서 몇 개의 속성을 선택해서 타입을 정의한다.

대상이 되는 타입과 Keys가 필요한데 이 Keys는 문자열 리터럴 혹은 문자열 리터럴의 합집합을 사용한다.

이렇게 말로만 하면 이해가 어려우니 아래 예제와 함께 살펴본다.

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

type TodoPreview = Pick<Todo, 'title' | 'completed'>;

const todo: TodoPreview = {
  title: 'Clean room',
  completed: false,
};

나의 풀이

대부분 유사하게 풀었을 것이라 생각한다.

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

우선 Pick과 동일한 구조를 만들어준다.

이후 Keys를 받는 부분의 제네릭은

  • T에 keyof를 사용해서 key들의 리터럴 값을 가져온다.
  • 해당 리터럴 값 앞에 extends를 넣어 K를 리터럴에 이미 있는 것으로 제한한다.

위 두 과정을 거쳐서 진행했다.

이후 내부의 경우에는 앞서 살펴본 매핑된 타입을 사용하고,

추가로 타입의 경우는 인덱스 접근을 통해서 T[key]와 같은 구조로 진행했다.


📕 7번 Readonly


문제 링크: https://github.com/type-challenges/type-challenges/blob/main/questions/00004-easy-pick/README.ko.md

이 문제의 경우 앞서서 살펴본 제네릭 매핑된 타입 예제를 통해서 힌트를 얻을 수 있다.

나의 풀이

이 또한 다들 유사하게 해결했을 것이다.

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

제네릭으로 받은 T를 in keyof 구문을 통해서 추출하고

앞에는 readonly 그리고 타입의 경우 인덱스 접근을 통한 T[key]로 해결을 했다.

이는 추가 설명은 하지 않겠다.


📕 11번 Tuple to Object


문제 링크: https://github.com/type-challenges/type-challenges/blob/main/questions/00004-easy-pick/README.ko.md

이 경우에는 조금 해결하는데 애를 먹었다.

근데 생각해보면 나름은 간단했다.

여기서는 얻어갈 지식이 우선 하나 있는데 바로 const 어서션이다.

const 어서션

const 어서션은 배열, 원시 타입, 값, 별칭 등 모든 값을 상수로 취급해야 함을 나타내는 데 사용된다.

특히 as const는 수신하는 모든 타입에 아래 3가지 규칙을 적용한다.

  • 배열은 가변 배열이 아니라 읽기 전용 튜플로 취급한다.
  • 리터럴은 일반적인 원시 타입과 동등하지 않고 리터럴로 취급된다.
  • 객체의 속성은 읽기 전용으로 간주된다.

간단한 예제로 살펴보면 아래와 같다.

[0, ' ']; // (number | string)[]
[0, ' '] as const; // readonly [0, ' ']

좀 더 실용적인 예제를 보면 아래와 같다.

function describePreference(preference: "maybe" | "no" | "yes"){
	switch(preference) {
		case "maybe":
			return "I suppose...";
		case "no":
			return "No thanks.";
		case "yes:
			return "Yes please!";
	}
}

const perferencesMutable = {
	movie: "maybe",
	standup: "yes",
};

describePreference(perferencesMutable.movie) // 이는 에러를 초래한다.

preferencesMutable.movie = "no"; // 내부적인 상태를 변화시킬 수 있다.

const perferencesReadonly = {
	movie: "maybe",
	standup: "yes",
} as const;

describePreference(perferencesMutable.movie) // 정상적으로 동작한다.

preferencesMutable.movie = "no"; // readonly 상태이기 때문에 에러가 발생한다.

간단한 함수가 있다고 가정하고 해당 함수는 매개 변수로 “maybe”, “no”, “yes” 만 받는다고 한다.

이때 우리가 만든 perferencesMutable을 인수로 넣어주려고 하면 에러가 발생한다.

이는 우리가 만든 perferencesMutable의 경우 string 타입으로 처리하기 때문에 3가지 종류만 받는 위 함수에 잘 적용되지 않는 것이다.

또한 우리가 내부에 접근해서 값을 변경하는 것 또한 가능한 것을 볼 수 있다.

하지만 as const로 선언한 부분을 보면 우리가 위에서 느낀 문제점을 한 번에 해결할 수 있다.

as const로 선언했기 때문에 함수에 인수로 잘 전달되는 것을 볼 수 있고,

또한 readonly 상태기 때문에 내부 값을 바꿀 수 없는 것 또한 볼 수 있다.

나의 풀이

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

앞서 살펴본 두 문제와 구조는 동일하다.

any[] 타입을 받고 이를 readonly extends를 통해서 T의 구조를 제한해둔다.

이후 key in 구문을 사용하는 것은 유사하나 대상이 튜플인 것이 문제였다.

이러한 튜플을 순회하려고 할 경우에 어떻게 하는지 살펴본 결과 number를 통해서 순회를 할 수 있다는 것을 알아냈다.

위 정보를 가지고 **key in T[number]**를 활용해 문제를 해결하였다.