Front-End(Web)/Typescript

[Type-challenges] 난이도 Easy - (1)

ttaeng_99 2023. 1. 24. 18:21
반응형

Github 챌린지 문제들을 풀고 관련된 내용을 정리하면서, 부족했던 타입스크립트 기본지식을 다지고자 한다. (주 1-2회)

 

https://github.com/type-challenges/type-challenges

 

GitHub - type-challenges/type-challenges: Collection of TypeScript type challenges with online judge

Collection of TypeScript type challenges with online judge - GitHub - type-challenges/type-challenges: Collection of TypeScript type challenges with online judge

github.com

 


📘 목차 - Easy

  1. Pick
  2. Readonly
  3. Tuple to Object
  4. First of Array
  5. Length of Tuple
  6. Exclude
  7. Awaited

 

 

 

📘 문제 및 풀이

 

1. Pick

Implement the built-in Pick<T, K> generic without using it.
Constructs a type by picking the set of properties K from T

For example:
interface Todo {
  title: string
  description: string
  completed: boolean
}

type TodoPreview = MyPick<Todo, 'title' | 'completed'>

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


/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<Expected1, MyPick<Todo, 'title'>>>,
  Expect<Equal<Expected2, MyPick<Todo, 'title' | 'completed'>>>,
  // @ts-expect-error
  MyPick<Todo, 'title' | 'completed' | 'invalid'>,
]

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

interface Expected1 {
  title: string
}

interface Expected2 {
  title: string
  completed: boolean
}

 

 

🖌 풀이

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

Pick 유틸리티 타입을 직접 작성하는 코드다. 제네릭의 인자를 각각 T(원본 타입), K(반환할 타입의 key들) 로 활용할 것이다.

 

반환할 타입을 Object로 먼저 설정해준다. 이 타입의 key를 P(타입)로 명시하고, key는 K에서, value는 T[P]로 가져온다.

마지막으로, K가 T의 키값임을 명시하기 위해 keyof T 타입을 extends로 "제약"시켜준다. (에러 케이스)

 

 

* keyof

Object 타입의 키값들을 유니온 타입으로 반환하는 문법

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

type PersonKeys = keyof Person; // "age" | "name"

 

 

* extends

통상적으로 interface의 확장이나 상속을 위한 문법으로 알고 있었다.

하지만, 제네릭 인자에 대해서는 타입을 제약하기 위한 문법으로도 사용된다. (참고링크)

추가로, extends keyof는 일부 키값만 활용해도 에러가 발생하지 않으나, in keyof 는 모든 키를 사용하지 않으면 에러가 발생하는 차이가 있었다.

 

 

 

2. Readonly

Implement the built-in Readonly<T> generic without using it.
Constructs a type with all properties of T set to readonly, meaning the properties of the constructed type cannot be reassigned.

For example:
interface Todo {
  title: string
  description: string
}

const todo: MyReadonly<Todo> = {
  title: "Hey",
  description: "foobar"
}

todo.title = "Hello" // Error: cannot reassign a readonly property
todo.description = "barFoo" // Error: cannot reassign a readonly property


/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<MyReadonly<Todo1>, Readonly<Todo1>>>,
]

interface Todo1 {
  title: string
  description: string
  completed: boolean
  meta: {
    author: string
  }
}

 

 

🖌 풀이

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

Readonly 유틸리티 타입을 직접 작성하는 코드다.

반환타입도 Object이며, key는 T의 in keyof로 가져오고 여기서 readonly 문법을 통해 읽기 전용 프로퍼티임을 명시해준다.

 

 

* readonly

읽기 전용 프로퍼티를 설정하기 위한 문법으로, 선언 후 변경이 불가능한 프로퍼티들을 설정하기 위해 사용된다.

Class 초기화, interface 키 값 등에 적용할 수 있다. (참고링크)

 

 

 

3. Tuple to Object

Given an array, transform it into an object type and the key/value must be in the provided array.

For example:
const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const

type result = TupleToObject<typeof tuple> // expected { 'tesla': 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}


/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const
const tupleNumber = [1, 2, 3, 4] as const
const tupleMix = [1, '2', 3, '4'] as const

type cases = [
  Expect<Equal<TupleToObject<typeof tuple>, { tesla: 'tesla'; 'model 3': 'model 3'; 'model X': 'model X'; 'model Y': 'model Y' }>>,
  Expect<Equal<TupleToObject<typeof tupleNumber>, { 1: 1; 2: 2; 3: 3; 4: 4 }>>,
  Expect<Equal<TupleToObject<typeof tupleMix>, { 1: 1; '2': '2'; 3: 3; '4': '4' }>>,
]

// @ts-expect-error
type error = TupleToObject<[[1, 2], {}]>

 

🖌 풀이

type TupleToObject<T extends readonly (string | number | symbol)[]> = {
  [P in T[number]]: P
}

튜플 타입을 받아서 각 요소를 key, value로 가지는 객체 타입을 반환해야한다.

 

T[number] 로 튜플의 각 요소를 인덱싱해서 P라는 타입으로 지정한 뒤, 이를 key와 value로 넣어준다.

제네릭에서 튜플타입을 받기 위해 readonly any[] 로 제약하지만, 에러 케이스처럼 튜플 요소들 외 타입을 제외하기 위해 any를 더 구체화한다. (string | number | symbol)[]

 

 

* Tuple(튜플)

튜플은 서로 다른 타입을 함께 가질 수 있는 배열이다. 튜플 타입을 선언할 때, 엘리먼트의 타입과 개수를 설정할 수 있다.

let tuple: [string, number]

tuple = ["hi", 200];		// ok!
tuple = ["hi", "400"];		// error

 

 

 

4. First of Array

Implement a generic First<T> that takes an Array T and returns its first element's type.

For example:
type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]

type head1 = First<arr1> // expected to be 'a'
type head2 = First<arr2> // expected to be 3


/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<First<[3, 2, 1]>, 3>>,
  Expect<Equal<First<[() => 123, { a: string }]>, () => 123>>,
  Expect<Equal<First<[]>, never>>,
  Expect<Equal<First<[undefined]>, undefined>>,
]

type errors = [
  // @ts-expect-error
  First<'notArray'>,
  // @ts-expect-error
  First<{ 0: 'arrayLike' }>,
]

 

🖌 풀이

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

제네릭으로 받은 배열(T)의 첫 번째 인자의 타입을 반환하면 된다.

단, 3번째 케이스처럼 빈 배열인 경우엔 never를 반환해야 하므로, 조건부 타입(Conditional Types) 으로 지정해준다.

 

 

* 조건부 타입(Conditional Types)

입력된 제네릭 타입에 따라 타입을 결정하는 문법이다. 삼항연산자와 비슷한 문법이며, extends 키워드로 타입을 체크한다. (참고문서)

 

 

 

5. Length of Tuple

For given a tuple, you need create a generic Length, pick the length of the tuple

For example:
type tesla = ['tesla', 'model 3', 'model X', 'model Y']
type spaceX = ['FALCON 9', 'FALCON HEAVY', 'DRAGON', 'STARSHIP', 'HUMAN SPACEFLIGHT']

type teslaLength = Length<tesla>  // expected 4
type spaceXLength = Length<spaceX> // expected 5


/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

const tesla = ['tesla', 'model 3', 'model X', 'model Y'] as const
const spaceX = ['FALCON 9', 'FALCON HEAVY', 'DRAGON', 'STARSHIP', 'HUMAN SPACEFLIGHT'] as const

type cases = [
  Expect<Equal<Length<typeof tesla>, 4>>,
  Expect<Equal<Length<typeof spaceX>, 5>>,
  // @ts-expect-error
  Length<5>,
  // @ts-expect-error
  Length<'hello world'>,
]

 

🖌 풀이

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

튜플은 배열처럼 보이지만 아래처럼 기술될 수 있다. 즉, 여기서 length 값이 튜플의 길이에 해당한다. (참고링크)

{
  0: 'sun',
  1: 'mon',
  2: 'tue',
  3: 'wed',
  4: 'thu',
  5: 'fri',
  6: 'sat',
  readonly length: 7,
}

 

 

 

6. Exclude

Implement the built-in Exclude<T, U>
* Exclude from T those types that are assignable to U

For example:
type Result = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'


/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<MyExclude<'a' | 'b' | 'c', 'a'>, 'b' | 'c'>>,
  Expect<Equal<MyExclude<'a' | 'b' | 'c', 'a' | 'b'>, 'c'>>,
  Expect<Equal<MyExclude<string | number | (() => void), Function>, string | number>>,
]

 

🖌 풀이

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

조건부 타입의 특징인 분산 조건부 타입(Distributive Conditional Types) 를 활용한 풀이이다.

유니온 타입(A | B | C)에 대해 조건부(T extends U ? X : Y)를 인스턴스화 하면 각 타입 인수에 대한 조건부로 걸린다. (참고링크)

(A extends U ? X : Y | B extends U ? X : Y | C extends U ? X : Y)

 

 

 

7. Awaited

If we have a type which is wrapped type like Promise. How we can get a type which is inside the wrapped type?

For example: if we have Promise<ExampleType> how to get ExampleType?
type ExampleType = Promise<string>

type Result = MyAwaited<ExampleType> // string


/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type X = Promise<string>
type Y = Promise<{ field: number }>
type Z = Promise<Promise<string | number>>
type Z1 = Promise<Promise<Promise<string | boolean>>>
type T = { then: (onfulfilled: (arg: number) => any) => any }

type cases = [
  Expect<Equal<MyAwaited<X>, string>>,
  Expect<Equal<MyAwaited<Y>, { field: number }>>,
  Expect<Equal<MyAwaited<Z>, string | number>>,
  Expect<Equal<MyAwaited<Z1>, string | boolean>>,
  Expect<Equal<MyAwaited<T>, number>>,
]

// @ts-expect-error
type error = MyAwaited<number>

 

🖌 풀이

type MyAwaited<T> = T extends PromiseLike<infer R>
  ? R extends PromiseLike<any>
    ? MyAwaited<R>
    : R
  : never

우선, T의 Promise 여부를 확인해서 아닐 경우 never를 반환한다. Promise라면 infer 키워드를 통해 내부의 타입을 추론한다.

type MyAwaited<T> = T extends Promise<infer InnerType> ? InnerType : never;

 

위 타입도 케이스들은 해결이 가능하나, 내부 타입이 Promise인 경우를 위해 안쪽 조건부처럼 MyAwaited를 재귀적으로 실행해준다.

 

 

* infer

infer는 조건부 타입의 extends 절에서 특정 지점의 타입을 추론하기 위해 사용되는 키워드다. (어렵고.. 나도 이해하는데 꽤 걸렸다..)

 

T extends infer U ? X : Y

기본적인 문법은 위와 같고, 기존 조건부 타입과 큰 차이가 안느껴지지만 제네릭이나 함수 등의 예제로 가면 유용해진다.

 

type Unpacked<T> =
    T extends (infer U)[] ? U :
    T extends (...args: any[]) => infer U ? U :
    T extends Promise<infer U> ? U :
    T;

type T0 = Unpacked<string>;  // string
type T1 = Unpacked<string[]>;  // string
type T2 = Unpacked<() => string>;  // string
type T3 = Unpacked<Promise<string>>;  // string
type T4 = Unpacked<Promise<string>[]>;  // Promise<string>
type T5 = Unpacked<Unpacked<Promise<string>[]>>;  // string

Unpacked 유틸리티 타입은 T를 제네릭 인자로 받는다.

이 T가 각각 배열인 경우엔 엘리먼트의, 함수인 경우 반환값의, 비동기(Promise)인 경우 반환값을 추론하는 타입이다.

 

타입을 직접 명시하는 게 아닌, 런타임 과정에서 타입을 추론하기 위해 이 키워드가 사용된다. (참고링크)

 

 

* Promise vs PromiseLike

Promise 타입을 다루기 위해 Promise 제네릭도 있지만, PromiseLike도 있다.

이 두 타입은 Promise<T> 는 finally만, PromiseLike<T> 는 then만 있다는 차이가 있다.

// lib.2018.promise.d.ts
interface Promise<T> {
  /**
   * Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The
   * resolved value cannot be modified from the callback.
   * @param onfinally The callback to execute when the Promise is settled (fulfilled or rejected).
   * @returns A Promise for the completion of the callback.
   */
  finally(onfinally?: (() => void) | undefined | null): Promise<T>
}

// lib.es5.d.ts
interface Promise<T> {
  /**
   * Attaches callbacks for the resolution and/or rejection of the Promise.
   * @param onfulfilled The callback to execute when the Promise is resolved.
   * @param onrejected The callback to execute when the Promise is rejected.
   * @returns A Promise for the completion of which ever callback is executed.
   */
  then<TResult1 = T, TResult2 = never>(
    onfulfilled?:
      | ((value: T) => TResult1 | PromiseLike<TResult1>)
      | undefined
      | null,
    onrejected?:
      | ((reason: any) => TResult2 | PromiseLike<TResult2>)
      | undefined
      | null,
  ): Promise<TResult1 | TResult2>

  /**
   * Attaches a callback for only the rejection of the Promise.
   * @param onrejected The callback to execute when the Promise is rejected.
   * @returns A Promise for the completion of the callback.
   */
  catch<TResult = never>(
    onrejected?:
      | ((reason: any) => TResult | PromiseLike<TResult>)
      | undefined
      | null,
  ): Promise<T | TResult>
}
interface PromiseLike<T> {
  /**
   * Attaches callbacks for the resolution and/or rejection of the Promise.
   * @param onfulfilled The callback to execute when the Promise is resolved.
   * @param onrejected The callback to execute when the Promise is rejected.
   * @returns A Promise for the completion of which ever callback is executed.
   */
  then<TResult1 = T, TResult2 = never>(
    onfulfilled?:
      | ((value: T) => TResult1 | PromiseLike<TResult1>)
      | undefined
      | null,
    onrejected?:
      | ((reason: any) => TResult2 | PromiseLike<TResult2>)
      | undefined
      | null,
  ): PromiseLike<TResult1 | TResult2>
}

그렇기에 이 둘의 가장 큰 차이는, 1) PromiseLike는 catch를 관여할 수 없다, 2) Promise는 비동기 반환 결과를 받을 수 없다 이다.

 

Promise가 정식 스펙이 되기 전 이를 구현한 라이브러리들이 있었고,

이들 중에 catch를 다루지 않는 타입을 지원하기 위해 PromiseLike가 별도로 출시된 것이다.

반응형