ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • TypeScript에서 (string & {}) 을 제외한 리터럴 유니언 타입만 남기기 🫥
    TypeScript 2025. 5. 31. 23:53

    TypeScript를 쓰다 보면 아래처럼 리터럴 유니언 타입을 정의하는 경우가 많다.

    type MyType = 'a' | 'b' | (string & {});
     

    그런데, 여기서 진짜 원하는 건 'a' | 'b' 만 남기고 string & {}은 제거하는 것일 수 있다. (더 엄격하게 타입을 체크하기 위해서)

     

    더 구체적으로는 다음과 같은 예제를 들 수 있다.

    type MyType = 'a' | 'b' | (string & {});
    type NoStringMyTime = 'a' | 'b';
    
    const data: MyType = 'a'
    
    function dataParse(value: NoStringMyTime) {
    
        console.log(value);
    }
    
    // Argument of type '(string & {}) | "a"' is not assignable to parameter of type 'NoStringMyTime'.
    dataParse(data);

     

     

    그럼 어떻게 string & {}을 제거해서 'a' | 'b'만 남길 수 있을까?


    이 문제는 단순한 유틸 타입으로 해결되지 않는다.

    그럼에도 불구하고, TypeScript의 분산 조건부 타입(distributive conditional types)을 응용하면 원하는 타입 유틸을 만들 수 있다.

    ✅ 목표: 'a' | 'b' | (string & {}) 에서 string을 제거해서 'a' | 'b'만 남기기

     

    🔬 솔루션> 넓은 타입 필터링 유틸리티 만들기

    type LiteralOnly<T> = T extends string ? string extends T ? never : T : T;

    해당 타입 유틸을 사용하면 이 문제를 해결할 수 있다.

     

    type MyType = 'a' | 'b' | (string & {});
    type NoStringMyTime = 'a' | 'b';
    
    type LiteralOnly<T> = T extends string ? string extends T ? never : T : T;
    
    const data: LiteralOnly<MyType> = 'a'
    
    function dataParse(value: NoStringMyTime) {
    
        console.log(value);
    }
    
    dataParse(data); // 에러 없음

     

    🔍 작동 방식 설명

    type LiteralOnly<T> = T extends string ? string extends T ? never : T : T;
    
    const data: LiteralOnly<MyType> = 'a'

    구체적으로 어떻게 동작하는지 확인해보자.

     

     

    1. T extends string: 일단 T가 string 계열인지 확인하고,
    2. string extends T: 이게 진짜 string 전체인지 판단한다.
      • string extends string → (넓은 string이 맞음) ✅
      • 'a' extends string → (리터럴도 string이니까)  
      • 하지만 'a'는 string의 슈퍼셋은 아니기 때문에 string extends 'a' → ❌

    즉, string extends T 가 참인 경우는 T가 진짜로 string일 때만 해당된다.
    그래서 그 경우는 never로 제거해주고, 'a' | 'b' 같은 리터럴들은 남게된다.

     

    💡 추가 예제 코드들

    type A = LiteralOnly<'a' | 'b' | (string & {})>; // 'a' | 'b'
    type B = LiteralOnly<'x' | 'y'>;         // 'x' | 'y'
    type C = LiteralOnly<string>;            // never
    type D = LiteralOnly<'z' | number>;      // 'z' | number (string만 제거됨)
     

    즉, string만 똑 떼어낼 수 있다.

    number, boolean 등은 그대로 남는다.

     

    🛠 그렇다면, string 말고 다른 넓은 타입도 제거하려면?

    type RemoveWider<T, W> = T extends W
      ? W extends T
        ? never
        : T
      : T;
    
    type OnlyLiterals = RemoveWider<'a' | 'b' | (string & {}) | number, string | number>;
    // 결과: 'a' | 'b'
     

    이렇게 하면 string과 number 모두 제거할 수 있다.

    ✨ 요약하자면,

    TypeScript는 정적 타입 시스템이면서도 엄청나게 유연하고 강력한 것 같다.

     

    이번 예제처럼 "넓은 타입을 제거하고 리터럴만 남기기" 같은 니치하지만 실무에선 꽤 유용할 수 있는 유틸도 만들 수 있었다.

     

    기존의 타입들을 최대한 활용할 수 있도록 유틸을 잘 작성하는 건 중요하다고 느끼는데, 이런 유틸 타입들에 대한 경험을 쌓아 나가는 것은 재밌는 일 같다 💘

     

    📎 참고

Designed by Tistory.