Front-End/TypeScript

[Type-challenges] 3, 8, 9, 10, 12

Y0ungZ 2024. 3. 2. 09:51
🍀 목차
3 - Omit
8 - Readonly 2
9 - Deep Readonly
10 - Tuple to Union
12 - Chainable Options

 

 

3 - Omit

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

// 예시
interface Todo {
  title: string
  description: string
  completed: boolean
}

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

const todo: TodoPreview = {
  completed: false,
}

 

/* _____________ 여기에 코드 입력 _____________ */

type MyOmit<T, K> = any

/* _____________ 테스트 케이스 _____________ */
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
}

 

  •  Exclude를 사용한 풀이
type MyOmit<T, K extends keyof T> = {
  [key in Exclude<keyof T, K>]: T[key];
}

 

Exclude<T, U>T extends U ? never : T이며, T에오는 U를 제외하겠다는 뜻이 된다.

 

  • Exclude를 사용하지 않은 풀이
type MyOmit<T, K extends keyof T> ={
  [key in keyof T as key extends K ? never : key] : T[key];
}

 

 Mapped Types와 Conditional Types를 사용한다. TypeScript 4.1 이상에서는 as를 통해 매핑된 타입의 키를 다시 매핑할 수 있다.

 

8 - Readonly 2

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

// 예시
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

 

/* _____________ 여기에 코드 입력 _____________ */

type MyReadonly2<T, K> = any

/* _____________ 테스트 케이스 _____________ */
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>>,
  Expect<Alike<MyReadonly2<Todo2, '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
}

 

 

 K가 주어지지 않으면 모든 프로퍼티를 읽기 전용으로 설정해야 하는 점을 유의해야 한다. 이 부분에서 막혀서 정답을 보고 공부하였다.

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

 

 우선 바로 위(3번) 문제였던 MyOmit을 재사용(유틸리티 타입 Omit이나 Pick을 사용해도 된다)하여 key가 K에 해당하는 프로퍼티라면 제외시킨다. 그렇지 않으면 key로 해당 프로퍼티를 포함시킨다.

이렇게 하면 T에서 K 프로퍼티만 제외하여 새로운 오브젝트가 만들어지는데, 이를 readonly [key in K] : T[key]&(Intersection)한다. 그렇게 K 프로퍼티만 readonly 시킨 결과가 나온다. 

 

 K가 없을 때를 위해 K를 추가로 keyof T로 설정한다. K가 명시되지 않으면 default parameter로 T의 프로퍼티를 주는 것이다.

 

 


9 - Deep Readonly

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

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

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`

 

/* _____________ 여기에 코드 입력 _____________ */

type DeepReadonly<T> = any

/* _____________ 테스트 케이스 _____________ */
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> = {
  readonly [key in keyof T] : keyof T[key] extends never ? T[key] : DeepReadonly<T[key]>
}

 

  T 객체의 모든 키를 반복하며 해당 키(key)에 대한 keyof T[key]를 검색하여(T[key]의 key를 검사) 없으면 해당 값을 반환하여 readonly로, 있다면 재귀적으로 다시 해당 key에 대해 DeepReadonly로 들어간다. 


10 - Tuple to Union

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

// 예시
type Arr = ['1', '2', '3']

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

 

/* _____________ 여기에 코드 입력 _____________ */

type TupleToUnion<T> = any

/* _____________ 테스트 케이스 _____________ */
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 readonly any[]> = T[number];

 


12 - Chainable Options

// 체인 가능 옵션은 일반적으로 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 로직을 작성할 필요는 없습니다. 단지 타입 수준입니다.

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

 

/* _____________ 여기에 코드 입력 _____________ */

type Chainable = {
  option(key: string, value: any): any
  get(): any
}

/* _____________ 테스트 케이스 _____________ */
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
}

 

Chainable<T> 타입은 option(key, value),  get() 두 함수를 갖고 있다.

  • option(key, value) : key, value를 입력받아 객체를 확장한다.
    • key는 string만, value는 무엇이든 올 수 있다.
    • 같은 key는 후에 선언된 value로 갱신된다.
  • get() : 현재까지 만들어진 객체를 반환한다. 

 

처음 작성한 답은 아래와 같다.

// 유틸리티 타입, 구현하지 않고 Record<K,V>를 사용해도 됨
type Record<K extends string, V> = {
  [key in K] : V;
}

type Chainable<T = {}> = {
  option: <K extends string, V>(key: K, value: V) => Chainable<T & Record<K,V>>
  get: () => T
}

 

 

 이 경우 result3이 Expected3과 달리 교차되며 string | number로 추론된다.

type Expected3 = {
  name: number
}

 

 그렇기 때문에 기존 존재하는 (key, value)를 제거시키고 신규로 입력된 (key, value)로 갱신시키는 부분이 필요하다. 

해당부분은 Omit을 사용하면 된다. 

// Omit과 Record는 구현하지 않아도 된다.
type Omit<T, K> = {
  [key in keyof T as key extends K? never: key] : T[key];
}

type Record<K extends string, V> = {
  [key in K] : V;
}

type Chainable<T = {}> = {
  option:<K extends string, V>(key:K, value:V) => Chainable<Omit<T,K> & Record<K,V>>
  get: () => T
}

 

 

참고자료

https://github.com/type-challenges/type-challenges/blob/main/questions/00003-medium-omit/README.ko.md

https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#key-remapping-via-as

https://github.com/type-challenges/type-challenges/blob/main/questions/00008-medium-readonly-2/README.ko.md

https://github.com/type-challenges/type-challenges/blob/main/questions/00009-medium-deep-readonly/README.ko.md

https://github.com/type-challenges/type-challenges/blob/main/questions/00010-medium-tuple-to-union/README.ko.md

https://github.com/type-challenges/type-challenges/blob/main/questions/00012-medium-chainable-options/README.ko.md