리터럴 타입 공통 속성(variant)으로 union 을 가르면 분기 안에서 좁혀진다.1 no-title 에 title 을 붙이면 막히는 것도 같은 축이다 — 초과 속성 검사가 variant 로 고른 멤버 하나에 대고 이뤄진다.2

import * as React from 'react'
import { IsEqual } from 'type-plus'

type ModalProps =
  | {
      variant: 'no-title'
    }
  | {
      variant: 'title'
      title: string
    }

export const Modal = (props: ModalProps) => {
  if (props.variant === 'no-title') {
    return <div>No title</div>
  } else {
    type Test = IsEqual<
      typeof props,
      {
        variant: 'title'
        title: string
      }
    >

    return <div>Title: {props.title}</div>
  }
}

export const Test = () => {
  return (
    <div>
      <Modal variant="title" title="Hello" />
      <Modal variant="no-title" />

      {/* @ts-expect-error */}
      <Modal />
      <Modal
        variant="no-title"
        // @ts-expect-error
        title="Oh dear"
      />
    </div>
  )
}

Footnotes

  1. TypeScript Handbook — Narrowing, Discriminated unions: “When every type in a union contains a common property with literal types, TypeScript considers that to be a discriminated union, and can narrow out the members of the union.” ↩

  2. 판별 속성이 없는 union({ a: number } | { b: string })은 { a: 1, b: 'x' } 를 받는다 — 3.5 릴리스 노트도 “partial overlap is still permitted” 라 한다. 막히는 건 검사 대상을 판별 속성으로 먼저 좁히기 때문이다: TypeScript v5.9.3 checker.ts — hasExcessProperties reducedTarget = findMatchingDiscriminantType(source, target, …). ↩

`#482`