Skip to content

Instantly share code, notes, and snippets.

@MichaelDimmitt
Last active December 16, 2022 21:53
Show Gist options
  • Save MichaelDimmitt/8eb075d14f5add22f8099b861312af85 to your computer and use it in GitHub Desktop.
Save MichaelDimmitt/8eb075d14f5add22f8099b861312af85 to your computer and use it in GitHub Desktop.
One good use for Discriminating Unions in Typescript

One good use for Discriminating Unions in Typescript

Today I learned, some new typescript today after facing a simple issue.

The issue I faced, I wanted a type that would enforce:

  1. if defined, both are required.
  2. else, none are required.
  3. bonus, you can have other discriminating unions require other types to be included instead of this one.

This would ensure developers do not miss one of the inputs when you need both for the change to work effectively.

Valid

<FileUpload
    accept="*"
/>
<FileUpload
    accept="*"
    isDirty={false}
    highlightDirty={true}
/>
<FileUpload
    accept="*"
    oops={'as a developer I force highlight dirty to be a number for an example'}
    highlightDirty={0}
/>

Invalid (typescript will throw an error tell you what is wrong)

<FileUpload
    accept="*"
    highlightDirty={true}
/>
<FileUpload
    accept="*"
    isDirty={false}
/>
<FileUpload
    accept="*"
    isDirty={false}
    highlightDirty={0}
/>

This can be accomplished with discriminating unions.

// If defined, both are required.

type IDirtyTrack = {
 highlightDirty?: undefined
 isDirty?: undefined
} | {
 highlightDirty: boolean,
 isDirty: boolean
}

interface FileUploadProps {
  /* Bunch of props I need here */
}

type ItWorks = FileUploadProps & IDirtyTrack

https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html#discriminating-unions

I have seen the discriminating unions like:

{ state: "failed" | "error"; } also: { cool: true; }

But never thought about this situation: { highlightDirty?: undefined }

Undefined here is a value both a value and a type here. 🤯 (Intentionally I say this wrong here.)

That is not true, actually everything here is a type.
Even { state: "failed" } is saying, I am a type of "failed" and only "failed".

We could expand this further to take numbers requiring different props:

// If defined, both are required.
type IDirtyTrack = {
 highlightDirty?: undefined
 isDirty?: undefined
} | {
 highlightDirty: boolean,
 isDirty: boolean
} | {
 highlightDirty: number,
 oops: string
}
interface FileUploadProps {
  /* Bunch of props I need here */
}
type ItWorks = FileUploadProps & IDirtyTrack

What fun!

Reposting scenerios from above for readability.

Valid

<FileUpload
    accept="*"
/>
<FileUpload
    accept="*"
    isDirty={false}
    highlightDirty={true}
/>
<FileUpload
    accept="*"
    oops={'as a developer I force highlight dirty to be a number for an example'}
    highlightDirty={0}
/>

Invalid (typescript will throw an error tell you what is wrong)

<FileUpload
    accept="*"
    highlightDirty={true}
/>
<FileUpload
    accept="*"
    isDirty={false}
/>
<FileUpload
    accept="*"
    isDirty={false}
    highlightDirty={0}
/>

Takeaway:

I could see this being really useful for libraries that need to have the same component used many different ways.

@Andrew-JBS
Copy link

That's cool, I'll have to remember this one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment