[TS] Type-Challenges 스터디 3주차
![[TS] Type-Challenges 스터디 3주차](/_next/image?url=https%3A%2F%2Fvelog.velcdn.com%2Fimages%2Fhayou%2Fpost%2Fa9669951-258e-44f7-aa7a-4b7dd00e02c2%2Fimage.jpg&w=3840&q=75)
[Medium] 2. Return Type
View on GitHub: https://tsch.js.org/2
문제
내장 제네릭 ReturnType<T>을 이를 사용하지 않고 구현하세요.
정답
type MyReturnType<T extends Function> = T extends (...args: any) => infer U
? U
: never;
설명
ReturnType<T>이란T의 반환 타입을 반환하는 제네릭 타입이다.- 우선,
T가Function타입인지 확인 - 맞다면 반환 타입을
infer U로 추론해 추론한 타입U를 반환 - 함수가 아니라면
never반환 - parameters와 마찬가지로
...args는 아무 이름이어도 되고, TypeScript에서 그냥 ‘추론할 위치가rest parameter임을 표시하기 위한 placeholder
추가 질문
`(...args:any)는 되고 (args: any)는 안되는 이유는?
(...args: any)형태는 0개 이상의 매개변수를 허용하며, 모든 매개변수는 배열 형태로 캡처(args: any)형태는 1개의 매개변수를 허용하며, 매개변수는 단일 값으로 캡처- 따라서 f1와 같이 2개 이상의 매개변수를 받기 위해서는
(...args: any)형태로 작성해야 함
Reference
[Medium] 3. Omit
View on GitHub: https://tsch.js.org/3
문제
T에서 K 프로퍼티만 제거해 새로운 오브젝트 타입을 만드는 내장 제네릭 Omit<T, K>를 이를 사용하지 않고 구현하세요.
정답
type MyOmit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
설명
- 우선
K extends keyof T를 통해K가T의 키 값임을 명시 P in keyof T를 통해T의 키 값들을 순회as를 통해P가K에 포함되지 않는 경우에만P를 반환never는 타입 시스템에서 제거 되기 때문에,P가K에 포함되지 않는 경우에만P: T[P]가 반환 됨
추가 질문
as딥 다이브
as를 활용한 키 리맵핑은 타입스크립트 4.1 버전에 추가된 기능- 매핑된 타입에
as절을 사용해서 매핑된 타입의 키를 다시 매핑할 수 있음
type MappedTypeWithNewProperties<Type> = {
[Properties in keyof Type as NewKeyType]: Type[Properties];
};
- 템플릿 리터럴 타입과 같은 기능을 활용해서 이전 프로퍼티에서 새로운 프로퍼티 이름을 만들 수 있음
type Getters<Type> = {
[Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property];
};
interface Person {
name: string;
age: number;
location: string;
}
type LazyPerson = Getters<Person>;
// type LazyPerson = {
// getName: () => string;
// getAge: () => number;
// getLocation: () => string;
// }
- 정답에서 사용했듯이 조건부 타입을 통해
never를 생성해서 키를 필터링할 수 있음 string | number | symbol의 조합뿐만 아니라 모든 타입의 조합을 임의로 매핑할 수 있음
Reference
[Medium] 8. Readonly 2
View on GitHub: https://tsch.js.org/8
문제
T에서 K 프로퍼티만 읽기 전용으로 설정해 새로운 오브젝트 타입을 만드는 제네릭 MyReadonly2<T, K>를 구현하세요. K가 주어지지 않으면 단순히 Readonly<T>처럼 모든 프로퍼티를 읽기 전용으로 설정해야 합니다.
정답
type MyReadonly2<T, K extends keyof T = keyof T> = {
readonly [P in keyof T as P extends K ? P : never]: T[P];
} & { [P in keyof T as P extends K ? never : P]: T[P] };
설명
- 3-omit 문제에서 사용한
as를 활용한 필터링 기능 사용 - 첫 번째 집합에는
K에 할당할 수 있는 T의 키 값인 P에 대해서는readonly키워드를 붙이고, 그렇지 않은 키 값에 대해서는 제거 - 두 번째 집합에는
K에 할당할 수 있는 T의 키 값인 P에 대해서는 제거, 그렇지 않은 키 값에 대해서는 기존 타입 유지 - 두 집합을 intersection 타입으로 합치면 원하는 타입이 완성
추가 질문
왜 두 집합을 union 타입으로 합치면 안되는걸까?
type MyReadonly2<T, K extends keyof T = keyof T> =
| {
readonly [P in keyof T as P extends K ? P : never]: T[P];
}
| { [P in keyof T as P extends K ? never : P]: T[P] };
- union 타입으로 합칠 경우 readonly 프로퍼티를 가진 객체와 그렇지 않은 객체로 분리됨
type Example = { foo: string; bar: number; baz: boolean };
type Result = MyReadonly2<Example, "foo" | "bar">;
// Result = { readonly foo: string; readonly bar: number } | { baz: boolean };
Intersection은 교집합인데, 왜 서로 다른 타입을 합치는 것처럼 보이는걸까?
-
첫 번째 집합과 두 번째 집합은 key가 서로 다른 객체이다.
-
그렇다면 intersection(교차) 타입은 두 집합의 교집합(공집합)을 반환해야 하는 것이 아닌가?
-
TypeScript에서 교차 타입은 교집합이 아니라 두 타입을 "결합"한 새로운 타입을 반환
-
두 개 이상의 타입을 조합하여 하나의 타입을 만들어 각 타입의 속성을 모두 포함하는 타입이 생성됨
type A = { foo: string };
type B = { bar: number };
type C = A & B;
// 결과 타입: { foo: string; bar: number }
- 교차 타입의 결합 방식 1) 두 타입에 동일한 키가 있는 경우
type A = { foo: string };
type B = { foo: string };
type C = A & B;
// 결과: { foo: string }
- 교차 타입의 결합 방식 2) 두 타입에 동일한 키가 있지만 값의 타입이 다른 경우
type A = { foo: string };
type B = { foo: number };
type C = A & B;
// 결과: { foo: never }
- 교차 타입의 결합 방식 3) 서로 다른 키를 가지는 경우
type A = { foo: string };
type B = { bar: number };
type C = A & B;
// 결과: { foo: string; bar: number }
- 교차 타입의 결합 방식 4) 함수 타입의 경우(모든 시그니처를 동시에 만족해야 함)
// 예시1
type A = (x: number) => string;
type B = (x: number) => number;
type C = A & B;
// C는 두 시그니처를 모두 만족해야 함
const fn: C = (x: number) => {
if (x > 0) return "string";
return 123; // 타입 에러 (반환 타입이 모두 만족하지 않음)
};
// 예시2
type A = (x: string) => number;
type B = (x: number) => string;
type C = A & B;
const fn: C = (x: any) => {
if (typeof x === "string") return x.length; // x가 string일 때 number 반환
if (typeof x === "number") return x.toString(); // x가 number일 때 string 반환
};
Reference
[Medium] 9. Deep Readonly
View on GitHub: https://tsch.js.org/9
문제
객체의 프로퍼티와 모든 하위 객체를 재귀적으로 읽기 전용으로 설정하는 제네릭 DeepReadonly<T>를 구현하세요.
이 챌린지에서는 타입 파라미터 T를 객체 타입으로 제한하고 있습니다. 객체뿐만 아니라 배열, 함수, 클래스 등 가능한 다양한 형태의 타입 파라미터를 사용하도록 도전해 보세요.
정답
type DeepReadonly<T> = T extends object
? T extends Function
? T
: { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
설명
- 첫 번째 조건부 타입은
T가 객체인 경우를 체크, 객체가 아닌 경우T를 반환 - 두 번째 조건부 타입은
T가 객체이지만 함수일 경우T를 반환 (해당 조건이 없을 경우, 함수일 경우 빈 객체를 반환) - 만약
T가 객체이고 함수가 아니라면 객체의 프로퍼티를 읽기 전용으로 설정한 타입을 반환
추가 질문
배열이 제대로 처리되는 이유
- 우선, 배열은 객체이자 함수가 아님.
- 따라서
T가 배열일 경우{ readonly [K in keyof T]: DeepReadonly<T[K]> }타입을 반환 - 배열에서
keyof는 인덱스와 특수키(length, push 등)를 포함함.
type TestArray = [1, 2, 3];
type Keys = keyof TestArray; // "0" | "1" | "2" | "length" | "push" | "pop" | "concat" | ...
- 인덱스를 통해 배열 요소에 접근, 각 요소에 대해 DeepReadonly를 재귀적으로 호출
- 특수 키의 경우 이미 읽기 전용으로 설정되어 있어 별도로 표시하지 않음
Reference
[Medium] 10. Tuple to Union
View on GitHub: https://tsch.js.org/10
문제
튜플 값으로 유니온 타입을 생성하는 제네릭 TupleToUnion<T>를 구현하세요.
정답
type TupleToUnion<T extends readonly any[]> = T[number];
해설
- 11-tuple-to-object에서 사용한
T[number]을 활용해 union 타입 반환
추가 질문
Reference
[Medium] 12. Chainable Options
View on GitHub: https://tsch.js.org/12
문제
체인 가능 옵션은 일반적으로 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<CurrentConfig = object> = {
option<OptionKey extends string, OptionValue extends any>(
key: Exclude<OptionKey, keyof CurrentConfig>,
value: OptionValue
): Chainable<Omit<CurrentConfig, OptionKey> & Record<OptionKey, OptionValue>>;
get(): CurrentConfig;
};
CurrentConfig를 통해 현재까지 설정된 객체를 추적CurrentConfig는 최초 타입 선언 시 빈 객체로 초기화option함수는 새로운 key-value 쌍을 추가- 이 때
OptionKey는string,OptionValue는any로 선언 Exclude<OptionKey, keyof CurrentConfig>을 통해 이미 존재하는 key를 제외Omit<CurrentConfig, OptionKey>을 통해 기존 객체에서 해당 key를 제거Record<OptionKey, OptionValue>을 통해 새로운 key-value 쌍을 추가Chainable타입은Omit과Record를 통해 중복되지 않은 key를 가진 새로운 객체를 반환get함수는 최종 결과를 반환
추가 질문
Reference
comments
loading…