-
[Type-challenges] 3, 8, 9, 10, 12Front-End/TypeScript 2024. 3. 2. 09:51
🍀 목차
3 - Omit
8 - Readonly 2
9 - Deep Readonly
10 - Tuple to Union
12 - Chainable Options3 - 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://www.typescriptlang.org/docs/handbook/2/mapped-types.html#key-remapping-via-as