[Type-challenges] 난이도 Medium - (6)
Github 챌린지 문제들을 풀고 관련된 내용을 정리하면서, 부족했던 타입스크립트 기본지식을 다지고자 한다. (주 1-2회)
https://github.com/type-challenges/type-challenges
📘 목차 - Medium
- RequiredByKeys
- Mutable
- OmitByType
- ObjectEntries
- Shift
- Tuple to Nested Object
📘 문제 및 풀이
1. RequiredByKeys
Implement a generic RequiredByKeys<T, K> which takes two type argument T and K.
K specify the set of properties of T that should set to be required. When K is not provided, it should make all properties required just like the normal Required<T>.
For example
interface User {
name?: string
age?: number
address?: string
}
type UserRequiredName = RequiredByKeys<User, 'name'> // { name: string; age?: number; address?: string }
import type { Equal, Expect } from '@type-challenges/utils'
interface User {
name?: string
age?: number
address?: string
}
interface UserRequiredName {
name: string
age?: number
address?: string
}
interface UserRequiredNameAndAge {
name: string
age: number
address?: string
}
type cases = [
Expect<Equal<RequiredByKeys<User, 'name'>, UserRequiredName>>,
Expect<Equal<RequiredByKeys<User, 'name' | 'age'>, UserRequiredNameAndAge>>,
Expect<Equal<RequiredByKeys<User>, Required<User>>>,
// @ts-expect-error
Expect<Equal<RequiredByKeys<User, 'name' | 'unknown'>, UserRequiredName>>,
]
🖌 풀이
type Merge<T> = {
[P in keyof T]: T[P]
}
type RequiredByKeys<T, K extends keyof T = keyof T> = Merge<{
[P in keyof T as P extends K ? P : never]-?: T[P]
} & {
[P in keyof T as P extends K ? never : P]: T[P]
}>
지난 풀이의 7. PartialByKeys 를 응용한 풀이이다. (링크)
단, Optional한 키값을 Required로 바꿔주기 위해 -? 문법을 사용했다.
* Mapping Modifiers
Mapped Type을 만들 때, 프로퍼티에 +와 - 연산자를 사용할 수 있다.
이는, ?(optional), readonly 등의 조건을 추가할지 말지 여부로, 아무것도 기입하지 않으면 +이므로 -는 이에 반대로 적용한다는 의미이다. (참고링크)
2. Mutable
Implement the generic Mutable<T> which makes all properties in T mutable (not readonly).
For example
interface Todo {
readonly title: string
readonly description: string
readonly completed: boolean
}
type MutableTodo = Mutable<Todo> // { title: string; description: string; completed: boolean; }
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
interface Todo1 {
title: string
description: string
completed: boolean
meta: {
author: string
}
}
type List = [1, 2, 3]
type cases = [
Expect<Equal<Mutable<Readonly<Todo1>>, Todo1>>,
Expect<Equal<Mutable<Readonly<List>>, List>>,
]
type errors = [
// @ts-expect-error
Mutable<'string'>,
// @ts-expect-error
Mutable<0>,
]
🖌 풀이
type Mutable<T extends object> = {
-readonly [P in keyof T]: T[P]
}
위의 Mapping Modifiers 를 활용한 풀이이다. 단, 에러 케이스처럼 객체 외 값이 들어오는 경우를 예외처리해준다.
3. OmitByType
From T, pick a set of properties whose type are not assignable to U.
For Example
type OmitBoolean = OmitByType<{
name: string
count: number
isReadonly: boolean
isEnable: boolean
}, boolean> // { name: string; count: number }
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
interface Model {
name: string
count: number
isReadonly: boolean
isEnable: boolean
}
type cases = [
Expect<Equal<OmitByType<Model, boolean>, { name: string; count: number }>>,
Expect<Equal<OmitByType<Model, string>, { count: number; isReadonly: boolean; isEnable: boolean }>>,
Expect<Equal<OmitByType<Model, number>, { name: string; isReadonly: boolean; isEnable: boolean }>>,
]
🖌 풀이
type OmitByType<T, U> = {
[P in keyof T as T[P] extends U ? never : P]: T[P]
}
마찬가지로, 지난 풀이의 4. PickByType을 응용하면 된다. (링크)
4. ObjectEntries
Implement the type version of Object.entries
For example
interface Model {
name: string;
age: number;
locations: string[] | null;
}
type modelEntries = ObjectEntries<Model> // ['name', string] | ['age', number] | ['locations', string[] | null];
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
interface Model {
name: string
age: number
locations: string[] | null
}
type ModelEntries = ['name', string] | ['age', number] | ['locations', string[] | null]
type cases = [
Expect<Equal<ObjectEntries<Model>, ModelEntries>>,
Expect<Equal<ObjectEntries<Partial<Model>>, ModelEntries>>,
Expect<Equal<ObjectEntries<{ key?: undefined }>, ['key', undefined]>>,
Expect<Equal<ObjectEntries<{ key: undefined }>, ['key', undefined]>>,
]
🖌 풀이
type ObjectEntries<T> = {
[P in keyof T]-?: [P, T[P] extends infer R | undefined ? R : T[P]];
}[keyof T];
먼저 Mapping Modifiers로 옵셔널 키값들을 제외해줬다.
그리고 Partial의 케이스에서, value가 옵셔널하게 undefined인 경우도 정상적으로 반영되도록 infer R | undefined 인 경우엔 R 타입만 빼내서 반영해준 것이다.
5. Shift
Implement the type version of Array.shift
For example
type Result = Shift<[3, 2, 1]> // [2, 1]
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
// @ts-expect-error
Shift<unknown>,
Expect<Equal<Shift<[]>, []>>,
Expect<Equal<Shift<[1]>, []>>,
Expect<Equal<Shift<[3, 2, 1]>, [2, 1]>>,
Expect<Equal<Shift<['a', 'b', 'c', 'd']>, ['b', 'c', 'd']>>,
]
🖌 풀이
type Shift<T extends any[]> = T extends [infer F, ...infer R] ? R : [];
지난 풀이의 1. Pop 문제보다 좀 더 쉽다. 배열의 나머지(R) 부분을 반환해주면 된다. 에러 케이스를 고려해 T를 배열로 지정한다. (링크)
6. Tuple to Nested Object
Given a tuple type T that only contains string type, and a type U, build an object recursively.
type a = TupleToNestedObject<['a'], string> // {a: string}
type b = TupleToNestedObject<['a', 'b'], number> // {a: {b: number}}
type c = TupleToNestedObject<[], boolean> // boolean. if the tuple is empty, just return the U type
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<TupleToNestedObject<['a'], string>, { a: string }>>,
Expect<Equal<TupleToNestedObject<['a', 'b'], number>, { a: { b: number } }>>,
Expect<Equal<TupleToNestedObject<['a', 'b', 'c'], boolean>, { a: { b: { c: boolean } } }>>,
Expect<Equal<TupleToNestedObject<[], boolean>, boolean>>,
]
🖌 풀이
type TupleToNestedObject<T, U> = T extends [infer F extends PropertyKey, ...infer R] ? { [P in F]: TupleToNestedObject<R, U> } : U
배열의 분리와, 첫 값인 F를 키값으로 해서 유틸리티를 재귀적으로 돌려 객체를 만들면 된다.
F를 PropertyKey로 지정한 이유는, 이를 처리하지 않으면 키값 타입(string, number, symbol)에 부합하지 않을 수 있다는 에러가 발생하기 때문이다.
* PropertyKey
Javascript 객체의 키 값으로 사용가능한 타입인 string, number, symbol의 유니온 타입이다. (참고링크)