Front-End/TypeScript

[Type-challenges] 296, 298, 459, 527, 529

Y0ungZ 2024. 3. 30. 05:29
🍀 목차
296 - Permutation
298 - Length of String
459 - Flatten
527 - Append to object
529 - Absolute

 

 

296 - Permutation

// 주어진 유니언 타입을 순열 배열로 바꾸는 Permutation 타입을 구현하세요.

// 예시
type perm = Permutation<'A' | 'B' | 'C'>; // ['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A', 'B'] | ['C', 'B', 'A']

 

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

type Permutation<T> = any

/* _____________ 테스트 케이스 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<Permutation<'A'>, ['A']>>,
  Expect<Equal<Permutation<'A' | 'B' | 'C'>, ['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A', 'B'] | ['C', 'B', 'A']>>,
  Expect<Equal<Permutation<'B' | 'A' | 'C'>, ['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A', 'B'] | ['C', 'B', 'A']>>,
  Expect<Equal<Permutation<boolean>, [false, true] | [true, false]>>,
  Expect<Equal<Permutation<never>, []>>,
]

 

 정답을 보며 공부했다.

 

 순열을 만들 때 일반적으로 재귀 알고리즘을 사용하듯이 여기서도 Permutation 타입을 재귀로 구현한다. 기저조건은 마지막 테스트 케이스처럼 never이라면 빈 배열을 리턴하는 것이다. 순열을 만들 수 없다는 뜻이다.

그런데 아래로 작성했을 때, Permutation<never>은 true가 아닌 never이 나왔다.

type Permutation<T> = T extends never ? true : false;

type test = Permutation<never>; // never

 

아래와 같이 작성해야 생각하던 true가 반환된다.

 

type Permutation<T> = [T] extends [never] ? true : false;

type test = Permutation<never>; // true

 

never extends never은 true지만 T extends never은 never로 반환되는 이유는 분산 조건부 타입과 관련이 있다.

해당 문서를 보면, 조건부 타입이 제네릭 타입에 작용하는 경우, 유니온 타입이 분산형이 된다고 한다. T가 1 | 2 | 3이라면 T extends never ? true : false(1 extends never ? true : false) | (2 extends never ? true : false) | (3 extends never ? true : false)가 되는 것이다. 그리고 이 결괏값을 묶어 다시 유니온 타입으로 반환한다.

 

 never은 유니온 시 빈 유니온으로 간주된다. a | never은 a가 되는 것이다. 조건부 분배할 때는 never을 무시하게 된다. 빈 유니온에 조건을 분배하는 것이 의미가 없기 때문이다. 그렇기 때문에 never이 포함된 조건부 분배에서는 never이 유지될 수 있도록 타입 T를 변경시켜줘야 한다. [T]T[]처럼 작성하면 never 타입이 유지되어 분배가 올바르게 수행된다. 

[T] extends [never]

 

마지막 테스트 케이스를 통과하기 위해 기본 틀을 아래와 같이 잡아준다.

type Permutation<T> = [T] extends [never] ? [] : true;

 

 true부분은 어떻게 작성해야 할까?

다시 한번, 조건부 타입에 유니온 타입을 주게 되면 분산되어 계산된다.

 

 만들어야 할 로직은 현재 요소를 고정하고 현재 요소를 제외한 나머지 요소들에 대해 재귀 호출 하는 것이다. T extends T 형식으로 작성하게 될 경우, 항상 true기에 결과의 의미는 없지만 각 요소에 대해 로직을 실행할 수 있게 해 준다.

 

 현재 요소를 제외한 나머지 요소를 표현할 때는 Exclude를 사용하면 된다. 

type Exclude<T,U> = T extends U ? never : T; // 유틸리티 타입 사용해도 된다.

 

 이제 default 값으로 T를 갖는 U를 추가해서 Exclude가 잘 될 수 있도록 한다.

type Exclude<T,U> = T extends U ? never : T;
type Permutation<T, U = T> = [T] extends [never] ? [] : U extends U ? [U, ...Permutation<Exclude<T,U>>] : never;

 


298 - Length of String

// String#length처럼 동작하는 문자열 리터럴의 길이를 구하세요.

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

type LengthOfString<S extends string> = any

/* _____________ 테스트 케이스 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<LengthOfString<''>, 0>>,
  Expect<Equal<LengthOfString<'kumiko'>, 6>>,
  Expect<Equal<LengthOfString<'reina'>, 5>>,
  Expect<Equal<LengthOfString<'Sound! Euphonium'>, 16>>,
]

 

 배열처럼 length 요소로 접근할 수 있지 않을까 생각했지만 문자열 리터럴 타입은 컴파일 시 상수로 처리되어 동적인 속성 접근을 허용하지 않는다고 한다. 그렇기에 배열로 변환하여 length 속성을 추출할 수 있다.

 

type LengthOfString<S extends string> = S['length'];

 

number로 추론된다.
number로 추론될 뿐이다.

 

type StringToArray<S> = S extends `${infer first}${infer rest}` ? [first, ... StringToArray<rest>] : [];
type LengthOfString<S extends string> = StringToArray<S>['length'];

 


459 - Flatten

// 주어진 배열을 플랫한 배열 타입으로 바꾸는 Flatten 타입을 구현하세요.

// 예시
type flatten = Flatten<[1, 2, [3, 4], [[[5]]]]> // [1, 2, 3, 4, 5]

 

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

type Flatten = any

/* _____________ 테스트 케이스 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<Flatten<[]>, []>>,
  Expect<Equal<Flatten<[1, 2, 3, 4]>, [1, 2, 3, 4]>>,
  Expect<Equal<Flatten<[1, [2]]>, [1, 2]>>,
  Expect<Equal<Flatten<[1, 2, [3, 4], [[[5]]]]>, [1, 2, 3, 4, 5]>>,
  Expect<Equal<Flatten<[{ foo: 'bar', 2: 10 }, 'foobar']>, [{ foo: 'bar', 2: 10 }, 'foobar']>>,
]

 

type Flatten<T> = 
T extends [] ? [] :
T extends [infer First, ...infer Rest] ? 
First extends any[] ? 
[...Flatten<First>,...Flatten<Rest>]
: [First, ...Flatten<Rest>]
: T;

 

527 - Append to object

// 주어진 인터페이스에 새로운 필드를 추가한 object 타입을 구현하세요. 
// 이 타입은 세 개의 인자를 받습니다.

// 예시
type Test = { id: '1' }
type Result = AppendToObject<Test, 'value', 4> // expected to be { id: '1', value: 4 }
/* _____________ 여기에 코드 입력 _____________ */

type AppendToObject<T, U, V> = any

/* _____________ 테스트 케이스 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type test1 = {
  key: 'cat'
  value: 'green'
}

type testExpect1 = {
  key: 'cat'
  value: 'green'
  home: boolean
}

type test2 = {
  key: 'dog' | undefined
  value: 'white'
  sun: true
}

type testExpect2 = {
  key: 'dog' | undefined
  value: 'white'
  sun: true
  home: 1
}

type test3 = {
  key: 'cow'
  value: 'yellow'
  sun: false
}

type testExpect3 = {
  key: 'cow'
  value: 'yellow'
  sun: false
  moon: false | undefined
}

type cases = [
  Expect<Equal<AppendToObject<test1, 'home', boolean>, testExpect1>>,
  Expect<Equal<AppendToObject<test2, 'home', 1>, testExpect2>>,
  Expect<Equal<AppendToObject<test3, 'moon', false | undefined>, testExpect3>>,
]

 

 

 기존 객체의 key-value는 아래와 같이 만들 수 있다.

type AppendToObject<T, U, V> = {
  [key in keyof T] : T[key]
};

 

 여기서 새로운 U라는 key값에 V라는 value를 넣어야 하므로

 

기존 key값이 아니라면 T[key]가 아닌 V로 연결해 주는 부분이 있어야 할 테고,

type AppendToObject<T, U , V> = {
  [key in keyof T] : key extends keyof T ? T[key] : V;
};

 

위의 로직은 당연히 현재 T의 key밖에 조회가 되지 않으니 유니온 타입을 사용하여 U도 key가 될 수 있음을 추가한다.

 

type AppendToObject<T, U , V> = {
  [key in keyof T | U] : key extends keyof T ? T[key] : V;
};

 

오류가 발생한다. 타입이 지정되지 않은 U는 string | number | symbol 이 될 수 있는 type keyof T에 할당할 수 없기 때문이다. U를 직접 keyof T의 타입과 동일하게 해 주거나, keyof any를 사용하여 모든 값의 유형을 나타낸다고 할 수 있다.

type AppendToObject<T, U extends string | number | symbol , V> = {
  [key in keyof T | U] : key extends keyof T ? T[key] : V;
};

// Or

type AppendToObject<T, U extends keyof any , V> = {
  [key in keyof T | U] : key extends keyof T ? T[key] : V;
};


529 - Absolute

// number, string, 혹은 bigint을 받는 Absolute 타입을 만드세요. 
// 출력은 양수 문자열이어야 합니다.

// 예시
type Test = -100
type Result = Absolute<Test> // expected to be "100"

 

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

type Absolute<T extends number | string | bigint> = any

/* _____________ 테스트 케이스 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<Absolute<0>, '0'>>,
  Expect<Equal<Absolute<-0>, '0'>>,
  Expect<Equal<Absolute<10>, '10'>>,
  Expect<Equal<Absolute<-5>, '5'>>,
  Expect<Equal<Absolute<'0'>, '0'>>,
  Expect<Equal<Absolute<'-0'>, '0'>>,
  Expect<Equal<Absolute<'10'>, '10'>>,
  Expect<Equal<Absolute<'-5'>, '5'>>,
  Expect<Equal<Absolute<-1_000_000n>, '1000000'>>,
  Expect<Equal<Absolute<9_999n>, '9999'>>,
]

 

type Absolute<T extends number | string | bigint> = `${T}` extends `-${infer U}` ? U : `${T}`;

 

 양수라면 유지, 음수라면 양수로 변환하여 출력한다. 문자열로 바꾸는 부분을 유의해야 한다.

 

 

참고자료

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

https://github.com/type-challenges/type-challenges/blob/main/questions/00298-medium-length-of-string/README.ko.md

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

https://github.com/type-challenges/type-challenges/blob/main/questions/00527-medium-append-to-object/README.ko.md

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