일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- typescript type challenge
- 이펙티브 타입스크립트
- typeorm
- 코딩테스트
- nestjs
- chainable options
- configmodule
- npm
- equal 타입
- 와글와글
- 모던자스
- 타입챌린지
- 회고
- type-safe configservice
- type challenge equal type
- 스터디
- network
- 백엔드
- HTTP
- 월간cs
- 타입 챌린지
- Type Challenge
- 해커톤
- configservice
- TypeScript
- TypeScript 타입챌린지
- 타입스크립트
- prisma 파일 분리
- 타입스크립트 타입챌린지
- node.js
- Today
- Total
iamkanguk.dev
[Type Challenge] Omit 문제에 대한 고찰 (readonly 관련) 본문
https://github.com/type-challenges/type-challenges/blob/main/questions/00003-medium-omit/README.md
TypeScript에서 지원하는 Utility Type인 Omit을 직접 구현해보는 문제였다.
해당 문제를 구현하면서 헷갈렸던 부분을 정리하려고 한다. Omit에 대한 자세한 설명을 하지는 않겠다.
(Omit<T, K> -> T에서 K들을 빼고 리턴하는 것>
풀이1)
type cases = [
Expect<Equal<Expected3, ThirdMyOmit<Todo1, 'description' | 'completed'>>>,
]
interface Todo1 {
readonly title: string
description: string
completed: boolean
}
interface Expected3 {
readonly title: string
}
// 메인 풀이
type FirstMyOmit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
- T의 Key들을 순회할 때 P라는 변수를 가지고 순회를 할건데 P가 K에 할당이 가능하다면 never를 통해 Index Signature에서 제외시키고, 할당이 불가능하다면 적용한다. 그리고 해당 경우에는 T를 P로 접근하여 value를 가져온다.
- 가장 직관적이고 무난한 풀이라고 생각이 된다.
풀이2) Exclude를 사용한 풀이 (오답)
type cases = [
Expect<Equal<Expected3, ThirdMyOmit<Todo1, 'description' | 'completed'>>>,
]
interface Todo1 {
readonly title: string
description: string
completed: boolean
}
interface Expected3 {
readonly title: string
}
// 메인 풀이
type SecondMyOmit<T, K extends keyof T> = {
[P in Exclude<keyof T, K>]: T[P];
};
- Omit은 T에서 K를 제외하고 반환하는 타입이기 때문에 Exclude를 사용할 수 있다는 의미이다.
- Exclude를 사용해서 T에서 K를 제외한 타입을 가져오고, 그것을 P라는 변수를 통해 순회해서 객체를 만들어준다는 의미이다.
- 하지만 해당 코드는 TypeScript Playground에서 에러를 발생시키고 있다. 이유는 무엇일까? 이후에 확인해보자.
에러가 발생하는 이유?
확인해보니 readonly를 인식하지 못해서 에러가 발생하는 것 같다.
그래서 왜 케이스를 통과를 못하는지 생각을 해보다가, keyof를 적용할 때 readonly 부분을 잡아주지 못해서 그랬다고 생각했다.
하지만 기본적으로 객체 안에서 keyof를 사용하면 readonly는 무시되지 않는 다는 것이다. 이게 무슨말인가?
interface Todo {
readonly title: string;
description: string;
}
type keyOfTodo = keyof Todo; // "title" | "description"
type Hello = {
[P in keyof Todo]: T[P]
}; // { readonly title: string; description: string; }
- Todo라는 타입에 keyof Todo를 하면 readonly가 제거된 상태로 결과가 출력된다.
- 하지만, Hello라는 타입을 보면 객체 안에서 keyof Todo를 하면 readonly가 유지된 채로 결과가 출력된다.
- 이 부분을 인지하지 못해서 헷갈렸던 것 같다.
그러면 다시 위의 코드가 에러가 발생하는 이유에 대해서 생각해보자.
Exclude는 결과 타입이 객체가 아닌 유니온 타입이다.
type Fruit = "cherry" | "banana" | "strawberry" | "lemon";
type RedFruit = Exclude<Fruit, "banana" | "lemon">;
// type RedFruit = "cherry" | "strawberry" 와 같다.
방금 위에서 객체 타입에서 사용한 keyof는 readonly가 유지된다고 언급했는데, Exclude의 결과 타입이 유니온 타입이기 때문에 readonly가 유지가 되지 않는다. 그래서 P in Exclude<keyof T, K> 를 하게 되면 readonly가 빠진 채로 순회가 되기 때문에 에러가 발생하고 있는 것이다.
풀이3) Exclude를 활용하면서 해결하는 방법
일단 왜 에러가 발생하는지는 파악이 되었다. 그러면 Exclude를 통해서 해결하는 방법이 있을까?
type cases = [
Expect<Equal<Expected3, ThirdMyOmit<Todo1, 'description' | 'completed'>>>,
]
interface Todo1 {
readonly title: string
description: string
completed: boolean
}
interface Expected3 {
readonly title: string
}
// 메인 풀이
type ThirdMyOmit<T, K extends keyof T> = {
[P in keyof T as Exclude<P, K>]: T[P];
};
아까 봤던 코드랑 약간 다르다. 일단 as 라는 키워드를 "그런데" 라고 해석해보자.
인지했으면 코드를 천천히 해석해보자.
먼저, Exclude<P, K>는 P extends K ? never : T; 라고 치환할 수 있을 것 같다.
치환한 뒤의 코드를 살펴보자.
type cases = [
Expect<Equal<Expected3, ThirdMyOmit<Todo1, 'description' | 'completed'>>>,
]
interface Todo1 {
readonly title: string
description: string
completed: boolean
}
interface Expected3 {
readonly title: string
}
// 메인 풀이
type ThirdMyOmit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : T]: T[P];
};
이렇게 보면 뭔가 조금씩 보인다.
1. keyof를 객체 내에서 사용했기 때문에 readonly 상태가 유지된다.
2. 유지되는 상태에서 P를 추출했기 때문에 P에는 readonly가 적용된 Property Key가 올 수 있다.
3. 그 P가 K에 할당될 수 있으면 빼고, 할당이 되지 않는다면 Index Signature 상태로 유지한다.
그래서 우리는 Exclude 유틸리티 타입을 사용해서 문제를 해결할 수 있었다.
최대한 다른 분들이 보기에 이해하기 쉽게 작성해봤는데 이해가 되실지는 모르겠다.
그래도 필자와 같이 해당 문제를 풀 때 이런 고민을 한적이 있다면 도움이 되었으면 좋겠다.
핵심은 객체 내에서의 keyof는 readonly를 유지할 수 있다는 것.
NestJS 오픈채팅 톡방에서 도움을 주신 Michael님 감사합니다.
- https://chanhuiseok.github.io/posts/ts-3/
- https://bkdragon0228.tistory.com/m/3
'Type Challenge' 카테고리의 다른 글
[Type Challenge] LastIndexOf + 이럴 때 Equal Type을 쓰는거구나? (0) | 2024.11.05 |
---|---|
[Type Challenge - MEDIUM] Chainable Options (0) | 2024.10.02 |
[타입챌린지-MEDIUM] 5153: IndexOf (0) | 2024.04.09 |
[타입챌린지-MEDIUM] 9: Deep Readonly (0) | 2024.04.04 |
[타입챌린지-MEDIUM] 8: Readonly 2 (0) | 2024.03.27 |