티스토리 뷰

 

평소처럼 한가롭게 개발을 이어가던 와중, React의 경량화 버전이라고 볼 수 있는 Preact에서 나의 자작 패키지 react_widgets의 타입 추론 기능(그에 따라 자동완성 기능이 제대로 동작하지 않음)이 Preact 기반 개발 환경에서 제대로 동작하지 않는다는 것을 깨달았습니다.

 

이에 대해 자세히 알아본 결과, Preact에서 CSS 속성이 정의된 인터페이스, 즉 CSSProperties를 Typescript에서 기본적으로 제공하는 유틸리티 타입인 Omit으로 랩핑한다면 어떤 이유인지 모르겠지만 CSSProperties의 모든 속성이 제외되는 현상이 발생한 것입니다.

 

이는 사실 대부분은 모두 아는 사실이겠지만 Omit 타입의 경우, 주어진 특정 속성만을 제외하는 기능을 수행합니다.

Omit<CSSProperties, "display"> // display 속성을 제외한 나머지

위 코드에서는 앞서 설명한 내용들을 토대로 CSSproperties의 display 속성 키를 제외한 나머지 속성 값이 정의된 인터페이스가 반환되어야 한다는 것을 알 수 있습니다. 다만, Preact에서 제공하는 Type Delcaration의 경우, 원하는 의도와는 달리 모든 속성 값이 제외되는 치명적인 결과를 초래하게 됩니다.


이러한 현상이 발생하게 된 근본적인 원인은 TypeScript의 유틸리티 타입인 Omit이 상속 및 중첩된 상태에서 인덱스 서명과 같은 기능을 사용할 때, 타입 추론 문제를 발생시킬 가능성이 높기 때문이며 이로 인해서 타입을 전혀 추론하지 못한 타입스크립트가 뻗어버린 것이였습니다.

해당 문제점이 아직 해결되지 않은 원인은 생각보다 간단합니다. Omit은 인덱스 서명이 도입되기 훨씬 이전에 개발되고 사용된 유틸리티 타입입니다. 따라서 이를 중간에 수정하거나 변경한다면 기존 레거시 코드에 영향을 끼칠 위험이 있기 때문에 이러한 문제점은 아직 해결되지 않았습니다. (또한 기존 Omit 또한 올바르지 않는 동작을 수행한다고는 정확하게 정의할수도 없기 때문)

interface A {
    hello: number;
    world: number;
    [key: string]: any;
}

type B = Omit<A, "hello">

위의 코드에서 올바른 동작은 B 타입이 A 인터페이스의 속성 중 hello를 제외한 나머지 속성만 온전히 정의된 인터페이스를 반환해야 하는 것입니다.

그러므로 hello를 제외한 world 속성은 undefined나 any 타입이 되어서는 안 됩니다. 그러나 실제로 이 코드에서는 B의 인터페이스에서 world 속성이 any 타입으로 변환되어버렸습니다.

 

해당 상황이 발생하는 주 원인은 인덱스 서명(index signature)에 의한 것입니다. 대충 인덱스 서명은 객체의 속성 키와 그 값의 타입을 미리 지정해주는 역할을 하는 간단한 기능을 수행합니다.

이제 본론으로 들어가자면, 타입스크립트에서 인덱스 서명이 포함된 인터페이스에서 특정 문제점이 발생하는 주된 이유는 인덱스 서명에서 정의된 타입이 모든 키와 값의 타입으로 추론되는 경향이 있기 때문입니다. 이로 인해 속성을 제외하는 과정에서 예상치 못한 타입 변형이 생기는 것입니다. 즉, 인덱스 서명은 객체의 속성 타입을 미리 선언하고 추론할 수 있게 하는 역할을 하기 때문에 이러한 기능이 객체의 타입을 재정의할 때에 영향을 끼쳐 이러한 문제를 발생시킵니다.

따라서 이러한 문제를 효과적으로 해결하거나 방지하기 위해서는, 기존의 Omit과 유사한 기능을 수행하는 새로운 타입을 별도로 선언해줄 필요가 있습니다.

export type DeepOmit<T, K extends keyof T> = {
    [P in keyof T as Exclude<P, K>]: T[P]
};

위의 코드에서 DeepOmit은 T의 모든 속성들을 반복하면서 K에 해당하는 속성들만 제외한 이후, 나머지 속성의 타입을 그대로 유지하도록 합니다. 기능 측면에서는 동일합니다만 여기서 기존 Omit과 다른 점은 속성 키와 값의 타입을 명시적으로 정의해 주는 것에 있습니다. 기존 Omit에서처럼 Exclude 유틸리티 타입을 이용하여 특정 속성을 제외하는 것은 똑같지만 여기서는 Exclude는 필터링 역할만을 수행하고 타입을 정의하는 역할을 수행하지는 않습니다.

 

따라서 이론상으로도 기존 속성 값의 타입이 변형되거나 하지 않는다는 것을 알 수 있습니다.

결론적으로, 타입스크립트에서 인덱스 서명이 포함된 타입을 사용할 때는 Omit 대신 위 코드처럼 별도의 타입을 선언하고 이를 사용하는 것이 더 바람직할 수 있으며 이를 통해 예상치 못한 기존 타입의 손상 및 여러 버그들을 방지할 수 있습니다.

 

대부분의 경우에서는 이러한 현상이 빈번하게 발생하지는 않지만 확실히 특정한 상황에서는 매우 도움이 되며 저 또한 몰랐으나 친절한 Preact 개발자분께서 알려주신 타입스크립트의 문제점들 중에 하나입니다.

 

이 글을 읽어주셔서 감사합니다.

 

관련 자료

Omit outputting incorrect types for types with mixed [key: string]: string | undefined and defined field types#

49656

 

Omit outputting incorrect types for types with mixed `[key: string]: string | undefined` and defined field types · Issue #49656

Bug Report Using Omit and [key: string]: string | undefined together can result in Typescript not preserving the types of fields on the original type. Playground link here to display the issue: pla...

github.com

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
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
글 보관함