T extends string 이 있어야 넘긴 값을 리터럴 그대로("") 줍는다1 — 그래서 '' 를 넣으면 Exclude<'', ''> 가 never 가 돼 막힌다.

import * as React from 'react'

export type NonEmptyString<T> = Exclude<T, ''>

type Props<T> = {
  src: NonEmptyString<T>
}

function Image<T extends string>({ src }: Props<T>) { // 제약을 빼면 T = string 으로 넓어져 '' 도 통과
  return <img src={src} alt="" />
}

function App() {
  return (
    <>
      {/* @ts-expect-error */}
      <Image src="" />

      <Image src="ab" />
    </>
  )
}

다만 string 변수를 넘기면 T = string 이라 Exclude<string, ''> 도 string — 값이 '' 여도 통과한다. 인자 타입이 리터럴 '' 일 때만 막는 가드다(const e: '' = '' 를 넘겨도 막힌다).


Footnotes

  1. 객체 속성에 넣은 리터럴은 보통 string 으로 넓어지는데(<T>(x: { a: T }) 에 { a: 'ab' } → string), 들어갈 자리의 타입(여기선 Exclude<T, ''>)이 string 제약을 가지면 리터럴로 남긴다. TypeScript v5.9.3 checker.ts — isLiteralOfContextualType: “If the contextual type is a type variable constrained to a primitive type, consider this a literal context for literals of that primitive type. For example, given a type parameter ‘T extends string’, infer string literal types for T.” ↩

`#483`