[Type-challenges] 난이도 Hard - (1)
Github 챌린지 문제들을 풀고 관련된 내용을 정리하면서, 부족했던 타입스크립트 기본지식을 다지고자 한다. (주 1-2회)
https://github.com/type-challenges/type-challenges
📕 목차 - Hard
- Simple Vue
- Currying
- Union to Intersection
- Get Required
- Get Optional
- Required Keys
- Optional Keys
📕 문제 및 풀이
1. Simple Vue
Implement a simpiled version of a Vue-like typing support.
By providing a function name SimpleVue (similar to Vue.extend or defineComponent), it should properly infer the this type inside computed and methods.
In this challenge, we assume that SimpleVue take an Object with data, computed and methods fields as it's only argument,
- data : a simple function that returns an object that exposes the context this, but you won't be accessible to other computed values or methods.
- computed : an Object of functions that take the context as this, doing some calculation and returns the result. The computed results should be exposed to the context as the plain return values instead of functions.
- methods : an Object of functions that take the context as this as well. Methods can access the fields exposed by data, computed as well as other methods. The different between computed is that methods exposed as functions as-is.
The type of SimpleVue's return value can be arbitrary.
const instance = SimpleVue({
data() {
return {
firstname: 'Type',
lastname: 'Challenges',
amount: 10,
}
},
computed: {
fullname() {
return this.firstname + ' ' + this.lastname
}
},
methods: {
hi() {
alert(this.fullname.toLowerCase())
}
}
})
🖌 풀이
declare function SimpleVue<
D extends Record<string, unknown>,
C extends Record<string, unknown>,
M extends Record<string, unknown>
>(options: {
data: (this: never) => D
computed: { [K in keyof C]: (this: D, ...args: unknown[]) => C[K] }
methods: {
[K in keyof M]: (
this: D & C & { [K in keyof M]: (...args: unknown[]) => M[K] }
) => M[K]
}
}): any
Vue 프레임워크의 기본 포멧을 타이핑하는 문제다. 풀이를 참고만 하자.
2. Currying
Currying is the technique of converting a function that takes multiple arguments into a sequence of functions that each take a single argument.
For example:
const add = (a: number, b: number) => a + b
const three = add(1, 2)
const curriedAdd = Currying(add)
const five = curriedAdd(2)(3)
The function passed to Currying may have multiple arguments, you need to correctly type it.
In this challenge, the curried function only accept one argument at a time. Once all the argument is assigned, it should return its result
* Currying : https://en.wikipedia.org/wiki/Currying
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
import { ExpectFalse, NotEqual } from '@type-challenges/utils'
let x = 1
let y = 1 as const
type cases1 = [
Expect<Equal<Integer<1>, 1>>,
Expect<Equal<Integer<1.1>, never>>,
Expect<Equal<Integer<1.0>, 1>>,
Expect<Equal<Integer<typeof x>, never>>,
Expect<Equal<Integer<typeof y>, 1>>,
]
🖌 풀이
type Curry<P extends readonly any[], R> = P extends [infer H, ...infer T]
? (arg: H) => Curry<T, R>
: R
declare function Currying<P extends readonly any[], R extends boolean>(
fn: (...args: P) => R
): P extends [] ? () => R : Curry<P, R>
먼저 Curry 라는 유틸리티를 만들어준다.
P(배열), R(반환타입) 2가지를 받으며, 배열요소가 존재하면, 이를 인자의 타입으로 하는 함수를 재귀한다.
Currying 함수타입엔 P, R 2가지 타입조건을 건다. 인자로 받는 함수(fn)의 매개변수들은 P(배열), 반환타입을 R로 설정한다.
이에 대해 Curry 유틸리티를 적용한다. (매개변수가 없는 경우에 대한 예외처리 포함)
3. UnionToIntersection
Implement the advanced util type UnionToIntersection<U>
For example
type I = Union2Intersection<'foo' | 42 | true> // expected to be 'foo' & 42 & true
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<UnionToIntersection<'foo' | 42 | true>, 'foo' & 42 & true>>,
Expect<Equal<UnionToIntersection<(() => 'foo') | ((i: 42) => true)>, (() => 'foo') & ((i: 42) => true)>>,
]
🖌 풀이
이는, Distributive Conditional Type(분배 조건부 타입) 과 Inference from conditional types(조건부 타입에서의 추론) 2가지 원리로 푸는 문제이다.
type UnionToIntersection<U> = (U extends any ? (arg: U) => any : never) extends (
arg: infer P
) => any
? P
: never
먼저, 유니온 타입(U) 에 extends를 걸면 유니온의 각 타입에 대해 조건문을 실행한다.(Distributive)
이를 매개변수로 하는 함수 타입들의 유니온으로 변환한다.
왜냐하면, 타입자체를 비교하면 U와 U의 단일타입은 extends가 true로 나오나, 매개변수가 U와 U의 단일타입으로 비교되면 false로 더 엄격한 비교가 가능하기 때문이다.
// 타입 자체비교
declare let b: string
declare let c: string | number
c = b // ✅
// 매개변수로 비교
type Fun<X> = (...args: X[]) => void
declare let f: Fun<string>
declare let g: Fun<string | number>
g = f // 💥 this cannot be assigned
다음으로, 이 함수 타입들의 유니온 각각에 다시 조건문을 실행한다.
단, 여기서는 매개변수 타입을 infer P로 한정지었으므로, 유니온에서 넘어오는 많은 타입들 중 현재의 단일타입에 대해서만 P가 할당된다.
이 개념을, contra-variant position 이라고 정의한다.
이렇게, contra-variance가 적용된 유니온 타입은 특정 타입에 명확히 접근했다 판단하고 타입스크립트는 intersection으로 엮어준다.
* 위 유틸리티가 어떻게 intersection으로 변환되는지 모호할 것이다. 해당 포스팅이 이해에 많은 도움이 된다.
4. Get Required
Implement the advanced util type GetRequired<T>, which remains all the required fields
For example
type I = GetRequired<{ foo: number, bar?: string }> // expected to be { foo: number }
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<GetRequired<{ foo: number; bar?: string }>, { foo: number }>>,
Expect<Equal<GetRequired<{ foo: undefined; bar?: undefined }>, { foo: undefined }>>,
]
🖌 풀이
type GetRequired<T extends {}> = {
[K in keyof T as T[K] extends Required<T>[K] ? K : never]: T[K]
}
T[K]와 T를 Required로 변환한 타입에서의 T[K]를 비교한다.
Optional 인 경우 T[K]가 타입과 undefined 의 유니온이고, Required로 변환한 타입은 단일타입이므로 이 땐 false가 되기에 키를 never로 분기하면 된다.
5. Get Optional
Implement the advanced util type GetOptional<T>, which remains all the optional fields
For example
type I = GetOptional<{ foo: number, bar?: string }> // expected to be { bar?: string }
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<GetOptional<{ foo: number; bar?: string }>, { bar?: string }>>,
Expect<Equal<GetOptional<{ foo: undefined; bar?: undefined }>, { bar?: undefined }>>,
]
🖌 풀이
type GetOptional<T> = {
[K in keyof T as T[K] extends Required<T>[K] ? never : K]: T[K]
}
위 4. Get Required 의 반대로 풀면 된다.
6. Required Keys
Implement the advanced util type RequiredKeys<T>, which picks all the required keys into a union.
For example
type Result = RequiredKeys<{ foo: number; bar?: string }>;
// expected to be “foo”
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<RequiredKeys<{ a: number; b?: string }>, 'a'>>,
Expect<Equal<RequiredKeys<{ a: undefined; b?: undefined }>, 'a'>>,
Expect<Equal<RequiredKeys<{ a: undefined; b?: undefined; c: string; d: null }>, 'a' | 'c' | 'd'>>,
Expect<Equal<RequiredKeys<{}>, never>>,
]
🖌 풀이
type RequiredKeys<T> = keyof {
[K in keyof T as T[K] extends Required<T>[K] ? K : never]: T[K]
}
위 4. Get Required 에서 keyof로 키만 가져오면 되는 문제다.
7. Optional Keys
Implement the advanced util type OptionalKeys<T>, which picks all the optional keys into a union.
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<OptionalKeys<{ a: number; b?: string }>, 'b'>>,
Expect<Equal<OptionalKeys<{ a: undefined; b?: undefined }>, 'b'>>,
Expect<Equal<OptionalKeys<{ a: undefined; b?: undefined; c?: string; d?: null }>, 'b' | 'c' | 'd'>>,
Expect<Equal<OptionalKeys<{}>, never>>,
]
🖌 풀이
type OptionalKeys<T> = keyof {
[K in keyof T as T[K] extends Required<T>[K] ? never : K]: T[K]
}
위 5. Get Optional 에서 keyof로 키만 가져오면 되는 문제다.