iamkanguk.dev

[Type Challenge] LastIndexOf + 이럴 때 Equal Type을 쓰는거구나? 본문

Type Challenge

[Type Challenge] LastIndexOf + 이럴 때 Equal Type을 쓰는거구나?

iamkanguk 2024. 11. 5. 11:45

사전지식

해당 글을 읽기 전에 Type Challenge에서 제공하는 Equal Type에 대한 이해도가 필요합니다.

아래 포스팅은 제가 열심히 작성해 본 글이니 확인 한번 하시면 좋을 것 같아요!

 

https://dev-iamkanguk.tistory.com/entry/TypeScript-Type-Challenge-Equal-Type-%EB%B6%84%EC%84%9D%ED%95%98%EA%B8%B0

 

[TypeScript + Type-Challenge] Equal Type 분석하기

Type Challenge를 풀어본 사람이라면 Equal Type을 Type Challenge 문제에서 유틸 함수로 제공을 해준다는 것을 알 수 있을 것이다.필자는 해당 문제를 풀면서 여러가지 궁금한 점이 있어서 이렇게 글을 쭉

dev-iamkanguk.tistory.com

문제

https://github.com/type-challenges/type-challenges/blob/main/questions/05317-medium-lastindexof/README.md

 

type-challenges/questions/05317-medium-lastindexof/README.md at main · type-challenges/type-challenges

Collection of TypeScript type challenges with online judge - type-challenges/type-challenges

github.com

풀이

import { Equal, Expect } from "../../utils"

namespace iamkanguk_lastIndexOf {
    export type LastIndexOf<T extends unknown[], U> = T extends [...infer F, infer L] ? L extends U ? F['length'] : LastIndexOf<F, U> : -1;
}

namespace TestCase {
    // OK
    type TC1 = Expect<Equal<iamkanguk_lastIndexOf.LastIndexOf<[1, 2, 3, 2, 1], 2>, 3>>;
    // OK
    type TC2 = Expect<Equal<iamkanguk_lastIndexOf.LastIndexOf<[2, 6, 3, 8, 4, 1, 7, 3, 9], 3>, 7>>;
    // OK
    type TC3 = Expect<Equal<iamkanguk_lastIndexOf.LastIndexOf<[0, 0, 0], 2>, -1>>;
    // NOPE
    type TC4 = Expect<Equal<iamkanguk_lastIndexOf.LastIndexOf<[string, 2, number, 'a', number, 1], number>, 4>>;
    // NOPE
    type TC5 = Expect<Equal<iamkanguk_lastIndexOf.LastIndexOf<[string, any, 1, number, 'a', any, 1], any>, 5>>;
}

 

- 문제는 그렇게 어렵지 않았다. 하나의 배열을 둘로 쪼개는데 앞에서부터 접근하는게 아니라 뒤에서부터 접근한다고 생각하면 쉬웠던 것 같다. 충분히 이해할 수 있을 것이라고 생각한다.

- 하지만 문제는 위의 코드를 보면 TC4와 TC5가 통과를 안한다는 것이다. 로직적으로 잘못된 부분이 없는데 왜 통과를 안하는 것일까?

- 결론은 L extends U 부분에서 문제가 발생할 수 있다는 것이다.

어느 부분에서 문제가 발생할 수 있는건지?

TC1부터 TC3은 literal 타입이기 때문에 extends를 해도 정확한 비교가 되기 때문에 괜찮다.

하지만 TC4와 TC5를 보면 U가 Primitive 타입(원시 타입)이다. 이는 extends로는 정확한 비교가 안되기 때문에 에러가 발생하고 있는 것이다.

 

지금부터는 위에서 구현한 로직대로 차근차근 설명해보도록 하겠다.

보시기 편하게 LastIndexOf 타입을 다시 보여드리겠습니다.

type LastIndexOf<T extends unknown[], U> =
	T extends [...infer F, infer L]
    	? L extends U
        	? F['length']
            : LastIndexOf<F, U>
        : -1;

 

예시1 - TC4)

결과값이 4가 출력되어야 한다. 제일 뒤에 있는 number 타입을 찾아야 하기 때문이다.

편의상 string은 s, number는 n으로 표현하겠다.

type TC4 = Expect<Equal<LastIndexOf<[string, 2, number, 'a', number, 1], number>, 4>>;
  F L L extends U(number) Finish? LastIndex Equal with U?
1 [s, 2, n, 'a', n] 1 1 extends number -> true YES 5 NO
2 - - - -    

 

표를 보면 이해가 되실지 모르겠지만 결론은 1 extends number가 true로 추론되어서 1의 index인 5를 가져온거고 4와 5는 다르기 때문에 틀렸다고 표시되는 것이다.

 

여기서 살짝 의문이 들 수 있다. 그러면 L extends U가 아니라 U extends L을 하면 통과되는거 아냐?

맞다. number extends 1은 false이기 때문에 다음 스텝으로 넘어갈 것이고 정상적으로 4를 가져올 것이다.

 

하지만 U extends L로 바꿔도 다음 예시에서 에러가 발생한다.

예시2 - TC5)

type TC5 = Expect<Equal<LastIndexOf<[string, any, 1, number, 'a', any, 1], any>, 5>>;

 

  F L L extends U(any) Finish? LastIndex Equal with U?
1 [s, any, 1, number, 'a', any] 1 1 extends any -> true YES 6 NO
2 - - - - - -

 

위랑 비슷한 상황이다. 1 extends any가 true로 추론되어서 1의 index인 6을 가져온 것이고 6과 5는 다르기 때문에 틀렸다고 표시된다.

U extends L로 바꾸면 어떻게 될까? 그러면 1 extends any가 아니라 any extends 1이 될 것이다. 이는 결과가 boolean이다.

 

여기서 나오는 결론은 뭐냐면 이런 상황이 발생될 때는 extends 보다 더 정확한 비교가 필요하다는 것이다.

그래서 우리는 소스코드를 다음과 같이 수정할 수 있다.

최종 소스코드

import { Expect } from "../../utils"

export type Equal<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false;

namespace iamkanguk_lastIndexOf {
    export type LastIndexOf<T extends unknown[], U>
    	= T extends [...infer F, infer L]
        	? Equal<U, L> extends true   // 여기!
            	? F['length']
                : LastIndexOf<F, U>
            : -1;
}

namespace TestCase {
    // OK
    type TC1 = Expect<Equal<iamkanguk_lastIndexOf.LastIndexOf<[1, 2, 3, 2, 1], 2>, 3>>;
    // OK
    type TC2 = Expect<Equal<iamkanguk_lastIndexOf.LastIndexOf<[2, 6, 3, 8, 4, 1, 7, 3, 9], 3>, 7>>;
    // OK
    type TC3 = Expect<Equal<iamkanguk_lastIndexOf.LastIndexOf<[0, 0, 0], 2>, -1>>;
    // OK
    type TC4 = Expect<Equal<iamkanguk_lastIndexOf.LastIndexOf<[string, 2, number, 'a', number, 1], number>, 4>>;
    // OK
    type TC5 = Expect<Equal<iamkanguk_lastIndexOf.LastIndexOf<[string, any, 1, number, 'a', any, 1], any>, 5>>;
}

 

L extends U 부분을 Type Challenge에서 제공하는 Equal Type을 적용하면

더욱 정확한 비교를 통해 정답처리를 받을 수 있다.

 

이전 포스팅에서도 좋은 참고자료를 첨부했지만

아래 링크를 확인해보면 Equal Type에 대한 다른 설명도 있으니 참고하면 좋을 것 같다.

https://github.com/type-challenges/type-challenges/discussions/9100#discussioncomment-6896958

 

Could you explain how does `Equal` type implementation work? · type-challenges type-challenges · Discussion #9100

type-challenges/utils/index.d.ts Line 7 in e3cc0d3 export type Equal<X, Y> =

github.com