· Jake Worth

Either/or typed props


Sometimes in a typed program, we’d like to say that a function can receive:

  • One thing
  • Another thing
  • Never both
  • Never neither

Imagine a functional component that processes user-entered copy. Sometimes it’s markdown, sometimes it’s just copy, and the typed component needs to handle all four conditions above. How is this done?

We can achieve this with a union type and optional never props:

app.tsx
type Props =
{markdown: string; copy?: never} | {markdown?: never; copy: string};
const MaybeMarkdown = ({markdown, copy}: Props) =>
markdown ? parseMarkdown(markdown) : <>{copy}</>;

Let’s see it in action.

console.log
// Markdown ✅
console.log(MaybeMarkdown({markdown: '### Important'})); // <h3>Important</h3>
// Just copy ✅
console.log(MaybeMarkdown({copy: 'Just information'})); // <>Just information</>
// Both: type error ❌
console.log(
MaybeMarkdown({copy: 'Just information', markdown: '### That conflicts'}),
); // Type error
// Neither: type error ❌
console.log(MaybeMarkdown({})); // Type error

Why does this work and matter? Optional props alone aren’t either/or:

app.tsx
type Open = {markdown?: string; copy?: string};
// This allows both and neither ❌

Mark the other prop never, and you can’t pass it:

app.tsx
type Closed = {markdown?: string; copy?: never};
// `copy` can't be passed ✅

TypeScript documentation - never