[Type-challenges] 3, 8, 9, 10, 12
🍀 목차
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://www.typescriptlang.org/docs/handbook/2/mapped-types.html#key-remapping-via-as