일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 31 |
- 굿바이 2024년
- 회고록
- 타입스크립트
- 스터디
- Type Challenge
- microsoft azure openai
- 2024년 회고록
- 와글와글
- 타입챌린지
- node.js
- configmodule
- 백엔드
- 해커톤
- TypeScript 타입챌린지
- 코딩테스트
- mysql boolean
- typescript type challenge
- network
- nestjs
- npm
- type-safe configservice
- HTTP
- 월간cs
- 회고
- TypeScript
- mysql
- 이펙티브 타입스크립트
- 타입 챌린지
- typeorm
- configservice
- Today
- Total
iamkanguk.dev
[TypeScript + Type-Challenge] Equal Type 분석하기 본문
Type Challenge를 풀어본 사람이라면 Equal Type을 Type Challenge 문제에서 유틸 함수로 제공을 해준다는 것을 알 수 있을 것이다.
필자는 해당 문제를 풀면서 여러가지 궁금한 점이 있어서 이렇게 글을 쭉 작성해보려고 한다.
아직 100% 완벽하게 이해하지 못해서 주변에 자문을 많이 구해보려고 한다 (명확한 답안을 찾지 못함)
https://github.com/type-challenges/type-challenges/blob/main/questions/00599-medium-merge/README.md
type-challenges/questions/00599-medium-merge/README.md at main · type-challenges/type-challenges
Collection of TypeScript type challenges with online judge - type-challenges/type-challenges
github.com
해당 문제를 처음에 필자는 다음과 같이 풀었다.
type Merge<F, S> = Omit<F, keyof S> & S
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type Foo = {
a: number
b: string
}
type Bar = {
b: number
c: boolean
}
type cases = [
Expect<Equal<Merge<Foo, Bar>, {
a: number
b: number
c: boolean
}>>
]
S의 Key들을 F에서 제외시키고 S와 Intersection을 하면 문제가 해결될 것이라고 생각했지만 Playground에서는 에러를 발생시키고 있었다. 그래서 도저히 왜 에러가 발생하는지 이해가 되지 않았고 다른 사람이 작성한 답안을 같이 확인해봤다.
type Merge<F, S> = {
[key in keyof F | keyof S]: key extends keyof S
? S[key]
: key extends keyof F
? F[key]
: never;
};
위 코드의 Merge 타입은 정상적으로 Playground에서 동작을 하고 있다. 왜 이 Merge 타입은 통과를 하는 것인지 이해가 되지 않았다.
(물론 위 Merge 타입이 잘 작성이 되었다는 것은 이해했다. 하지만 Omit 타입을 사용한 것도 잘 작성이 되었다고 생각했다)
그래서 필자는 이 시점에서 2가지 생각이 들었다.
1. 필자가 Omit 타입을 사용해 작성한 답안이 애초에 틀린 답안인 경우
2. Type Challenge에서 제공하는 유틸 타입인 Equal 타입이 잘못된 경우
위 두 케이스를 각각 알아보도록 하자.
1. 필자가 Omit 타입을 사용해 작성한 답안이 잘못된 경우
내가 Omit 타입을 잘못 사용했나? 라는 생각이 들어 다시 한번 답안을 확인했지만 내 머리로는 특별히 로직상 문제가 없는 것 같다고 판단했다.
그리고 해당 답이 이슈 탭에서 가장 많은 따봉을 받은 답이었어서 특별히 문제가 되는 답은 아닐거라고 생각했다.
근데 나는 왜 틀리다고 나오지.. 뭔가 나랑 다르니까 22명의 사람들이 따봉을 누른게 아닐까?
그래서 어쩌면 이 사람들은 Playground에서 작업하지 않고 직접 구현한 Equal 타입을 사용하는게 아닐까라는 약간의 의심을 해봤다.
2. Type Challenge에서 제공하는 유틸 타입인 Equal 타입이 잘못된 경우
먼저 확인해보니 Equal Type에 대한 이야기가 많은 것 같다.
아래는 TypeScript 공식 GitHub의 Issue로 올라온 내용이다.
https://github.com/microsoft/TypeScript/issues/27024#issuecomment-421529650
[Feature request]type level equal operator · Issue #27024 · microsoft/TypeScript
Search Terms Type System Equal Suggestion T1 == T2 Use Cases TypeScript type system is highly functional. Type level testing is required. However, we can not easily check type equivalence. I want a...
github.com
Type Challenge에서 유틸성 타입으로 사용된 Equal 타입은 아래와 같다.
export type Equal<X, Y> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false;
위의 타입을 어떻게 이해해야 할까? 아직 이해를 100% 하지는 못했지만..
아래 두 링크를 참고하면 이해할 수 있을 것이라고 생각된다.
결론만 말하면 X extends Y ? true : false 랑 비슷하지만 차이점을 두기 위한 것이라고 생각하자.
true extends boolean은 true이지만 Equal 하지는 않은 것이 사실이다. Equal 하지 않다는 것을 표현하기 위해 저 기법을 사용했다고 지금은 그렇게 생각하자 (더욱 깊은 수준에서 엄격하게 타입을 비교할 수 있다?)
- https://suloth.tistory.com/36
타입챌린지 : Equal - 번외
이 글은 제가 타입챌린지를 하면서 해석한 내용을 적는 글입니다. 틀린 내용이 있으면 댓글 달아주시면 감사하겠습니다. 타입챌린지에서 항상 사용하는 타입이 있다. 그것은 바로 Equal. 그래서
suloth.tistory.com
- https://kscodebase.tistory.com/643
Equal type 설명하기
[Feature request]type level equal operator · Issue #27024 · microsoft/TypeScript Search Terms Type System Equal Suggestion T1 == T2 Use Cases TypeScript type system is highly functional. Type level testing is required. However, we can not easily check ty
kscodebase.tistory.com
위의 코드가 대충 어떤 식으로 구현이 되었는지를 파악했다면 이제 저 Equal 타입이 과연 잘못된 타입일까? 를 의심해봐야 한다.
결론적으로는 잘못된 건 아니지만 100% 정확한지는 모른다는 것이다.
How does the `Equals` work in typescript?
I found an Equals utils mentioned at: https://github.com/microsoft/TypeScript/issues/27024#issuecomment-421529650 export type Equals<X, Y> = (<T>() => T extends X ? 1 : 2) extend...
stackoverflow.com
위 StackOverflow를 보면 Equal 타입에 대해 굉장히 잘 분석해준 답변이 있다. 시간 내서 천천히 해석하면서 읽어보는 것을 강추한다.
해당 내용을 보면 왜 Type Challenge에서 { x: 1 } & { y: 2 } 와 { x: 1, y: 2 } 가 왜 Equal 하지 않다는 것이 이해가 될 것이다.
(물론 이것도 100% 정답은 아닐 수 있다는 가정이다)
지금부터 조금 길 수도 있지만 최대한 짧게 Stack Overflow에 올라온 답변에 대해 핵심만 정리를 해보려고 한다.
리마인드 차원에서 다시 코드를 여기에 작성해보고 시작하도록 하겠다.
export type Equal<X, Y> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false;
(1) 해당 타입은 두 개의 함수 타입을 비교해서 동등한 지 확인하는 것이다.
위 문법은 화살표 함수에 제네릭을 입힌 것이라고 보면 된다. 해당 문법이 이해가 되지 않는다면 아래 타입스크립트 핸드북을 읽어보자.
https://www.typescriptlang.org/docs/handbook/2/generics.html
여기 지점에서 필자의 고민과 동일하게 왜 저렇게 함수 타입을 비교하는 걸까? 다른 방법은 없을까? 라고 생각할 수 있다.
필자도 100% 이해한 건 아니지만, X extends Y 와 같은 기법으로 대체해서 사용하면 문제가 되는 부분이 발생하기 때문에 조금 더 엄격하게 비교하기 위한 기법이다! 라고 정도만 이해를 하고 넘어갔다.
몇 가지 예시를 같이 들어보자.
declare let x: <T>() => (T extends number ? 1 : 2)
declare let y: <T>() => (T extends number ? 1 : 2)
y = x // Should this be an error or not?
x와 y 변수에 다음과 같이 타입을 할당했다. 우리는 number 타입과 number 타입이 동일한지를 보고 싶은 것이다.
그리고 y에 x를 할당할 수 있는지를 확인했다. 이것이 우리가 원하는 Equal 로직이 아닌가?
위의 코드를 보면 Type Checker가 정상적으로 인식을 한다.
이번에는 number 타입과 string 타입을 비교해보자.
declare let x: <T>() => (T extends /*1st*/ number ? 1 : 2)
declare let y: <T>() => (T extends /*3rd*/ string ? 1 : 2)
y = x // It will occur error!
위의 코드는 이전 예시와 다르게 체커가 에러를 뱉을 것이다. 왜냐하면 x의 함수 타입과 y의 함수 타입이 불일치하기 때문이다.
위의 내용에 살을 조금 더 붙여보자. 아래 코드에서 주석으로 붙은 번호 순서대로 설명을 해보겠다.
declare let x: <T>() => (T extends number ? 1 : 2)
declare let y: <T>() => (T extends string ? 1 : 2)
const a = x<string>() // --- (1)
const b = x<number>() // --- (2)
const c = y<string>() // --- (3)
const d = y<number>() // --- (4)
y = x; --- (5)
const e = y<string>(); --- (6)
const f = y<number>(); --- (7)
1. 제네릭에 string 타입을 할당했기 때문에 string extends number가 수행될 것이다. string이 number에 할당이 가능하지 않기 때문에 2가 저장될 것이다.
2번-4번: 1번 내용을 이해했다면 충분히 해결할 수 있을 것이라고 생각한다.
5. x와 y가 동일한지 비교하기 위해 y에 x를 할당해보는 것이다. 아마 에러가 발생할 것이다. 그렇지만 일단 넘어가자.
6. y에 x를 할당했다고 가정해보자. 그러면 결국 x<string>()과 동일하다. y에 x가 정상적으로 할당되었다고 한다면 x<string>()과 y<string>()의 결과는 동일해야 한다. 하지만 x<string>()의 결과는 2, y<string>()의 결과는 1로 동일하지 않다.
7. 6번의 내용을 이해하면 이해할 수 있을 것이다.
Stack Overflow 답변 작성자님은 위와 같은 이유 때문에 extends 뒤에 오는 X와 Y가 동일해야 정상적인 결과가 반환되는 것이라고 말하고 있는 것이다.
(2) 결국 우리가 궁금한 것은 왜 { x: 1 } & { y: 2 } 그리고 {x : 1, y: 2} 를 비교했을 때 false 라는 거야?
결론만 말하자면 flag 비교로 인해 false가 발생한다는 것이다.
아래 코드는 TypeScript에서 수행하는 Type Checker의 소스 중 일부이다.
function isTypeRelatedTo(source: Type, target: Type, relation: /* ... */) {
// ...
if (source === target) {
return true;
}
if (relation !== identityRelation) {
// ...
}
else {
if (source.flags !== target.flags) return false;
if (source.flags & TypeFlags.Singleton) return true;
}
// ...
}
위의 소스코드에서 눈여겨 봐야할 점은 Type 타입이 무엇이고, flag가 의미하는게 뭐냐는 것이다.
아래 코드를 보면 이해될 것이다. TypeFlags에 Intersection 이 정의가 되어있다는 것이다.
export interface Type {
flags: TypeFlags; // 여기!
symbol: Symbol;
pattern?: DestructuringPattern;
aliasSymbol?: Symbol;
aliasTypeArguments?: readonly Type[];
}
export enum TypeFlags {
// ...
Object = 524288,
Intersection = 2097152,
}
source에는 { x: 1 } & { y: 2 } 가 들어갔고, target에는 { x: 1, y: 2 } 가 할당되었다고 생각해보자.
그러면 위의 코드를 보면 source.flags는 intersection 타입이기 때문에 해당하는 값인 2097152가 할당될 것이고, target.flags는 단순 object literal이기 때문에 object 타입으로 524288이 할당될 것이다.
두 값을 비교해보면 동일하지 않기 때문에 false가 반환되는 것이고 그래서 위의 Equal 타입에서는 false가 보여지는 것이다.
(3) 그런데 아무리 생각해도 이해가 안대져?
당연하다. 이론적으로 intersection 적용한 것과 일반 object literal 타입을 비교했을 때 동일한 게 맞다.
그냥 Type Challenge에서는 이런 형태의 Equal 타입을 추구하고 있다는 것과 해당 Equal 타입은 좀 더 엄격하게? 유연하게? 타입을 비교하는 것이다 정도로만 생각해도.. 괜찮지 않을까 싶다. 더 딥하게 파고들면 굉장히 머리아프다..
(왜 엄격하게? 유연하게? 라고 표현했냐면 Intersection을 적용한 타입과 literal로 표기한 타입을 비교했을 때 false라고 하는 것을 엄격하다고 표현해야 하는지 유연하다 라고 표현해야 하는지를 잘 모르겠어서 이렇게 작성했다. StackOverflow에서는 lazy approach 라고 표기한다!)
이에 대한 추가 예시로 해당 글에 댓글로 제로초님이 unknown과 any를 비교하는 것에 대해서도 질문을 하신 것 같은데 자세한 내용은 참고하면 좋을 것 같다!
그리고 이거는 필자가 따로 해본건데 true와 boolean을 비교하는 것도 한번 해보길 바란ㄷr.
(boolean이 true | false로 치환되어 제네릭 타입에 할당되어 분배 조건부 타입이 적용된다는 건 처음알았음)
3. 마무리
이정도로 글을 마무리하려고 한다. 아직 100% 이해했다고 말은 못하지만, 이 정도로만 학습하려고 한다.
검토를 몇번 하긴 했지만 해당 글에서 틀린 부분이 몇 개 있을 것이라고 생각이 드는데...
관심이 있거나 Type Challenge를 평소에 푸는 개발자 분들은 필자가 위에 참조 걸어놓은 블로그들과 Stack Overflow 내용을 한 번은 읽어보면 좋을 것 같다고 생각한다.
언제든 따끔한 피드백은 환영입니다! 감사합니다 :)
필자가 올린 issue입니다. 아마 댓글이 안달릴 것 같지만 이 포스팅을 보시는 개발자 분들 중에 흥미가 있다면 한번 필자의 글을 읽어보고 답을 달아주시면 대단히 감사하겠습니다!_!
https://github.com/type-challenges/type-challenges/issues/34874
Difference between the Equal Type implemented in the Type Challenge Repository and the Equal Type in this issue. · Issue #34874
I have a question while solving the Merge problem of the medium level. Here is the source code I implemented the Merge type. type Merge<F, S> = Omit<F, keyof S> & S /* _____________ Test Cases ____...
github.com
참고자료
- https://github.com/microsoft/TypeScript/issues/27024#issuecomment-421529650
[Feature request]type level equal operator · Issue #27024 · microsoft/TypeScript
Search Terms Type System Equal Suggestion T1 == T2 Use Cases TypeScript type system is highly functional. Type level testing is required. However, we can not easily check type equivalence. I want a...
github.com
'Language > TypeScript' 카테고리의 다른 글
[TypeScript] T extends U ? X : Y 는 정확히 어떤 의미일까? (0) | 2024.03.16 |
---|---|
[TS] 빈 객체에는 어떤 타입을 사용해야 할까요? (0) | 2024.02.07 |