Front-End(Web)/Typescript

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

ttaeng_99 2023. 2. 6. 01:53
반응형

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

 


📘 목차 - Medium

  1. Get Return Type
  2. Omit
  3. Readonly2
  4. Deep Readonly
  5. Tuple to Union
  6. Chainable Options
  7. Last of Array
  8. Pop

 

 

📘 문제 및 풀이

 

1. Get Return Type

Implement the built-in ReturnType<T> generic without using it.

For example
const fn = (v: boolean) => {
  if (v)
    return 1
  else
    return 2
}

type a = MyReturnType<typeof fn> // should be "1 | 2"


/* _____________ Your Code Here _____________ */

type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

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

type cases = [
  Expect<Equal<string, MyReturnType<() => string>>>,
  Expect<Equal<123, MyReturnType<() => 123>>>,
  Expect<Equal<ComplexObject, MyReturnType<() => ComplexObject>>>,
  Expect<Equal<Promise<boolean>, MyReturnType<() => Promise<boolean>>>>,
  Expect<Equal<() => 'foo', MyReturnType<() => () => 'foo'>>>,
  Expect<Equal<1 | 2, MyReturnType<typeof fn>>>,
  Expect<Equal<1 | 2, MyReturnType<typeof fn1>>>,
]

type ComplexObject = {
  a: [12, 'foo']
  bar: 'hello'
  prev(): number
}

const fn = (v: boolean) => v ? 1 : 2
const fn1 = (v: boolean, w: any) => v ? 1 : 2

 

🖌 풀이

type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

ReturnType 유틸리티 타입을 직접 구현하는 코드다. (Easy에서 풀었던 Parameters와 유사하다, 참고 포스팅)

T가 함수타입이면 infer로 명시한 반환값의 타입을 반환하고, 아니면 never(혹은 false)를 반환한다.

 


2. Omit

Implement the built-in Omit<T, K> generic without using it.

Constructs a type by picking all properties from T and then removing K

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

type TodoPreview = MyOmit<Todo, 'description' | 'title'>

const todo: TodoPreview = {
  completed: false,
}


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

type cases = [
  Expect<Equal<Expected1, MyOmit<Todo, 'description'>>>,
  Expect<Equal<Expected2, MyOmit<Todo, 'description' | 'completed'>>>,
]

// @ts-expect-error
type error = MyOmit<Todo, 'description' | 'invalid'>

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

interface Expected1 {
  title: string
  completed: boolean
}

interface Expected2 {
  title: string
}

 

🖌 풀이

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

우선 반환타입 부분을 먼저 보자. (이 부분으로도 테스트 케이스들은 통과되나, 에러를 검출하지 못한다.)

P라는 프로퍼티 타입이 K에 포함되면 never, 아니면 P를 key로 설정한다. 여기에 value를 T[P]로 할당하면 된다. (P를 as로 단언)

 

단, 에러처럼 K에 T의 키값들 외의 값이 포함되는 경우를 걸러내야 하므로, T, K 각각에 extends로 타입을 제한시켜준다.

 

 

* as + mapped type

 

as는 타입단언을 위해 보통 사용된다.

하지만, 이와 같은 mapped type과 같이 사용할 땐, P(프로퍼티 타입)의 타입을 단언하는 문법으로도 활용 가능하다. (문서링크)

 


3. Readonly 2

Implement a generic MyReadonly2<T, K> which takes two type argument T and K.
K specify the set of properties of T that should set to Readonly. When K is not provided, it should make all properties readonly just like the normal Readonly<T>.

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

const todo: MyReadonly2<Todo, 'title' | 'description'> = {
  title: "Hey",
  description: "foobar",
  completed: false,
}

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


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

type cases = [
  Expect<Alike<MyReadonly2<Todo1>, Readonly<Todo1>>>,
  Expect<Alike<MyReadonly2<Todo1, 'title' | 'description'>, Expected>>,
  Expect<Alike<MyReadonly2<Todo2, 'title' | 'description'>, Expected>>,
]

// @ts-expect-error
type error = MyReadonly2<Todo1, 'title' | 'invalid'>

interface Todo1 {
  title: string
  description?: string
  completed: boolean
}

interface Todo2 {
  readonly title: string
  description?: string
  completed: boolean
}

interface Expected {
  readonly title: string
  readonly description?: string
  completed: boolean
}

 

🖌 풀이

type MyReadonly2<T, K extends keyof T = keyof T> = {
  readonly [P in K]: T[P]
} & {
  [P in Exclude<keyof T, K>]: T[P]
}

이 문제는 Readonly 유틸리티를 구현하면서, K에 속한 프로퍼티들에만 readonly를 적용해야 한다는 조건이 있다.

먼저 전체 프로퍼티에 readonly를 적용하고, K에 속하지 않는 프로퍼티에만 readonly를 미적용하는 타입을 교차(Intersection)한다.

단, 테스트 케이스 1번처럼 유니온 2번째 인자(K)가 없는 경우에는 기본값을 keyof T로 설정해주는 처리를 K에 해주면 된다.

 


4. Deep Readonly

Implement a generic DeepReadonly<T> which make every parameter of an object - and its sub-objects recursively - readonly.
You can assume that we are only dealing with Objects in this challenge. Arrays, Functions, Classes and so on do not need to be taken into consideration. However, you can still challenge yourself by covering as many different cases as possible.

For example:
type X = { 
  x: { 
    a: 1
    b: 'hi'
  }
  y: 'hey'
}

type Expected = { 
  readonly x: { 
    readonly a: 1
    readonly b: 'hi'
  }
  readonly y: 'hey' 
}

type Todo = DeepReadonly<X> // should be same as `Expected`


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

type cases = [
  Expect<Equal<DeepReadonly<X1>, Expected1>>,
  Expect<Equal<DeepReadonly<X2>, Expected2>>,
]

type X1 = {
  a: () => 22
  b: string
  c: {
    d: boolean
    e: {
      g: {
        h: {
          i: true
          j: 'string'
        }
        k: 'hello'
      }
      l: [
        'hi',
        {
          m: ['hey']
        },
      ]
    }
  }
}

type X2 = { a: string } | { b: number }

type Expected1 = {
  readonly a: () => 22
  readonly b: string
  readonly c: {
    readonly d: boolean
    readonly e: {
      readonly g: {
        readonly h: {
          readonly i: true
          readonly j: 'string'
        }
        readonly k: 'hello'
      }
      readonly l: readonly [
        'hi',
        {
          readonly m: readonly ['hey']
        },
      ]
    }
  }
}

type Expected2 = { readonly a: string } | { readonly b: number }

 

🖌 풀이

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

마찬가지로 이 문제는 Readonly 유틸리티를 구현하면서, 객체의 value 값이 객체인 경우까지 대응하는 문제이다.

값이 Object면 DeepReadonly 유틸리티를 재귀적으로 적용해주면 된다.

 


5. Tuple to Union

Implement a generic TupleToUnion<T> which covers the values of a tuple to its values union.

For example
type Arr = ['1', '2', '3']

type Test = TupleToUnion<Arr> // expected to be '1' | '2' | '3'


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

type cases = [
  Expect<Equal<TupleToUnion<[123, '456', true]>, 123 | '456' | true>>,
  Expect<Equal<TupleToUnion<[123]>, 123>>,
]

 

🖌 풀이

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

T가 배열임을 명시하고, T[number] 로 각 인덱스에 접근하면 유니온이 된다.

 


6. Chainable Options

Chainable options are commonly used in Javascript. But when we switch to TypeScript, can you properly type it?
In this challenge, you need to type an object or a class - whatever you like - to provide two function option(key, value) and get(). In option, you can extend the current config type by the given key and value. We should about to access the final result via get.

For example
declare const config: Chainable

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

// expect the type of result to be:
interface Result {
  foo: number
  name: string
  bar: {
    value: string
  }
}


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

declare const a: Chainable

const result1 = a
  .option('foo', 123)
  .option('bar', { value: 'Hello World' })
  .option('name', 'type-challenges')
  .get()

const result2 = a
  .option('name', 'another name')
  // @ts-expect-error
  .option('name', 'last name')
  .get()

const result3 = a
  .option('name', 'another name')
  // @ts-expect-error
  .option('name', 123)
  .get()

type cases = [
  Expect<Alike<typeof result1, Expected1>>,
  Expect<Alike<typeof result2, Expected2>>,
  Expect<Alike<typeof result3, Expected3>>,
]

type Expected1 = {
  foo: number
  bar: {
    value: string
  }
  name: string
}

type Expected2 = {
  name: string
}

type Expected3 = {
  name: number
}

 

 

🖌 풀이

type Chainable<T extends Object = {}> = {
  option<K extends string, V>(key:K extends keyof T ? never : K, value: V)
    : Chainable<
      Omit<T, K> &
      { [k in K] : V }>
  get(): T
}

먼저 Chainable은 반환타입(T) 을 제네릭으로 받으며, 이는 Object 타입이자 기본값 {} 을 할당해준다.

또한, get() 메서드는 이 반환타입(T) 을 반환하면 된다.

 

option() 메서드는 K(key, 문자열), V(value, any) 2개 타입을 제네릭으로 우선 넣어준다.

key는 에러 케이스처럼 중복할당을 방지하기 위해, T에 이미 있는 값인 경우 never로 예외처리를 해준다.

최종적으로, option() 메서드는 Chainable 타입을 반환하며, T에서 K 외의 컬럼들과, K는 V를 할당한 컬럼을 반환타입으로 제네릭에 넣어주면 된다.

 


7. Last of Array

Implement a generic Last<T> that takes an Array T and returns its last element.

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

type tail1 = Last<arr1> // expected to be 'c'
type tail2 = Last<arr2> // expected to be 1


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

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

 

 

🖌 풀이

type Last<T extends any[]> = T extends [...infer X, infer R] ? R : any

생각보다 간단하게 풀 수 있었다. 마지막 인자를 infer R 로 접근할 수 있다.

 


8. Pop

Implement a generic Pop<T> that takes an Array T and returns an Array without it's last element.

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

type re1 = Pop<arr1> // expected to be ['a', 'b', 'c']
type re2 = Pop<arr2> // expected to be [3, 2]


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

type cases = [
  Expect<Equal<Pop<[3, 2, 1]>, [3, 2]>>,
  Expect<Equal<Pop<['a', 'b', 'c', 'd']>, ['a', 'b', 'c']>>,
  Expect<Equal<Pop<[]>, []>>,
]

 

 

🖌 풀이

type Pop<T extends any[]> = T extends [...infer X, infer Y] ? X:[]

위의 Last of Array를 조금 응용하면 된다. 단, 마지막 케이스처럼 빈 배열인 경우에 [] 를 그대로 반환해주면 된다.

 

반응형