{"pageProps":{"note":{"id":35244,"site_id":57,"user_id":63,"body":"# Generic discriminated union narrowing\n\n![organized tools](https://photos.collectednotes.com/photos/63/eb2238a2-6c46-45e6-8959-5305ee3ab4d3)\n\n> This post was originally a note on my [Digital Garden 🌱](https://publish.obsidian.md/gillchristian)\n\nWhen working with discriminated unions, also known as tagged unions, in [TypeScript](https://publish.obsidian.md/gillchristian/Research/TypeScript) one often has to narrow down a value of the union, to the type of one of the members.\n\n```ts\ntype Creditcard = {tag: 'Creditcard'; last4: string}\n\ntype PayPal = {tag: 'Paypal'; email: string}\n\ntype PaymentMethod = Creditcard | PayPal\n```\n\nGiven the above type, we would want to either check that a value is of PayPal type to show the user's email or Creditcard type to show the last 4 digits.\n\nThis can be do simply but checking the discriminant property, in this case `tag`. Since each of the two members of the `PaymentMethod` type have different types for the `tag` property, TypeScript is able to, well, discriminate between them to narrow down the type. Thus the *tagged union* name.\n\n```ts\ndeclare const getPaymentMethod: () => PaymentMethod\n\nconst x = getPaymentMethod()\n\nif (x.tag === 'Paypal') {\n console.log(\"PayPal email:\", x.email)\n}\n\nif (x.tag === 'Creditcard') {\n console.log(\"Creditcard last 4 digits:\", x.last4)\n}\n```\n\nTo avoid this explicit check of the `tag` property you might define [type predicates](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates), or guards.\n\n```ts\nconst isPaypal = (x: PaymentMethod): x is PayPal => x.tag === 'Paypal'\n\nconst x = getPaymentMethod()\n\nif (isPayPal(x)) {\n console.log(\"PayPal email:\", x.email)\n}\n```\n\nThe problem with such guards is they have to be defined for each member of each discriminated union type in your application. Or *do they*?\n\nIf you are like me you probably wonder how to do this generically for all union types. Could this be even possible with some TypeScript type level black magic?\n\nThe short answer is yes.\n\n```ts\nconst isMemberOfType = <\n Tag extends string,\n Union extends {tag: Tag},\n T extends Union['tag'],\n U extends Union & {tag: T}\n>(\n tag: T,\n a: Union\n): a is U => a.tag === tag\n```\n\nThe proper response is, what the ~~f\\*ck~~ ~~heck~~ hack?\n\nOk, let me explain.\n\nWe need four generic types, in practice the caller will not need to pass these, they are there with the purpose of allowing TypeScript to do its type inference business.\n\nNotice that for all of them we are not allowing any type but instead using `extends` to inform TypeScript that those generic types should meet certain conditions.\n\n- `Tag` is the union of all the tags of `Union`, it should extend the base type of our tag, in this case `string`\n- `Union` is our tagged union type, it has to be an object with a `tag` property of type `Tag`\n- `T` is the specific tag we want to narrow down with, and thus it should extend the `Tag` type\n- And `U` is the specific member of `Union` we want to narrow down to, it should extend the `Union` and its tag should be `T`, the specific one we want\n\n`Tag` and `Union` are there to define the union type we want to narrow down from and `T` and `U`, for the ~~lack~~ laziness of finding better names, are where the magic happens since they are the narrowed down types we expect to get to.\n\n```ts\nconst x = getPaymentMethod()\n\nif (isMemberOfType('Creditcard', x)) {\n console.log('Creditcard last 4 digits:', x.last4)\n}\n\nif (isMemberOfType('Paypal', x)) {\n console.log('PayPal email:', x.email)\n}\n```\n\n*Et voilà*, it works!\n\n![inferred creditcard type](https://photos.collectednotes.com/photos/63/7529fd81-1138-4360-a81a-c87d65fbd4bd)\n\nAnd this is how TypeScript fills in the *holes* of the generics.\n\n![inferred type arguments](https://photos.collectednotes.com/photos/63/ff441eb1-e044-4e22-97b1-2821742c1452)\n\nThere's a variation of `isMemberOfType` that I like to use which works as a sort of getter instead of type predicate.\n\n```ts\nconst getMemberOfType = <\n Tag extends string,\n Union extends {tag: Tag},\n T extends Union['tag'],\n U extends Union & {tag: T}\n>(\n tag: T,\n a: Union\n): U | undefined =>\n isMemberOfType(tag, a) ? a : undefined\n```\n\nThe usage is similar, but it requires a null check, of course.\n\n```ts\nconst cc = getMemberOfType('Creditcard', getPaymentMethod())\n\nif (cc) {\n console.log('Creditcard last 4 digits:', cc.last4)\n}\n\nconst pp = getMemberOfType('Paypal', getPaymentMethod())\n\nif (pp) {\n console.log('PayPal email:', pp.email)\n}\n```\n\nThere’s a little problem, the inferred type it returns is not so nice, since it's based on our definition of the generic (`U extends Union & {tag: T}`).\n\n![inferred returned type of getter](https://photos.collectednotes.com/photos/63/bf825eb9-a82c-490e-b893-9c835e7b5db7)\n\nIn practice this is not a problem, but since we got this far, we can keep going, right?\n\nEnter [Extract](https://www.typescriptlang.org/docs/handbook/utility-types.html#extracttype-union), one of TypeScript's type utilities:\n\n> Constructs a type by extracting from `Type` all union members that are assignable to `Union`.\n\nFor example, in the following case `T0` will be of type `'a'`:\n\n```ts\ntype T0 = Extract<'a' | 'b' | 'c', 'a' | 'f'>\n```\n\nThe definition is simple:\n\n```ts\n// Extract from Type those types that are assignable to Union\ntype Extract = Type extends Union ? Type : never\n```\n\n![slaps roof of car meme](https://photos.collectednotes.com/photos/63/204e9c1d-c050-433a-a583-be737ff70fe6)\n\nWith our current definition of `isMemberOfType` and `getMemberOfType`, the returned type extends union: `U extends Union & {tag: T}`.\n\nIn the case of PayPal it would be `PayPal & { tag: 'PayPal' }`. By adding `Extract` to the returned type we can get `PayPal` instead.\n\n\n```ts\nconst isMemberOfType = <\n Tag extends string,\n Union extends {tag: Tag},\n T extends Union['tag'],\n U extends Union & {tag: T}\n>(\n tag: T,\n a: Union\n): a is Extract => a.tag === tag\n\nconst getMemberOfType = <\n Tag extends string,\n Union extends {tag: Tag},\n T extends Union['tag'],\n U extends Union & {tag: T}\n>(\n tag: T,\n a: Union\n): Extract | undefined =>\n isMemberOfType(tag, a) ? a : undefined\n```\n\n![better inferred return type](https://photos.collectednotes.com/photos/63/78d2280d-a28a-4be3-9cec-6e4bd264a615)\n\nMuch cleaner this way! Now I can sleep in peace …\n\nIn conclusion, we went from simple discriminant property check to a monstrosity of generics and type inference that achieves the same thing. Should you use these utilities in production? ~~Of course! Even more so if it will confuse our colleagues.~~ Maybe not, but it was fun to discover some of the cool stuff we can achieve with TypeScript.\n\n[Here's the final version of the examples in the TypeScript Playground](https://tsplay.dev/NBRQbN).","path":"generic-discriminated-union-narrowing","headline":"organized tools\n\n\n\nThis post was originally a note on my Digital Garden 🌱\n\n\nWhen working with discriminated...","title":"Generic discriminated union narrowing","created_at":"2022-08-30T13:52:57.202Z","updated_at":"2022-08-31T08:35:32.585Z","visibility":"public","poster":"https://photos.collectednotes.com/photos/63/eb2238a2-6c46-45e6-8959-5305ee3ab4d3","curated":false,"ordering":0,"collections_id":null,"url":"https://collectednotes.com/gillchristian/generic-discriminated-union-narrowing"},"site":{"id":57,"user_id":63,"name":"Christian Gill","headline":"","about":"","host":null,"created_at":"2020-05-20T07:58:35.178Z","updated_at":"2023-09-17T17:34:17.741Z","site_path":"gillchristian","published":true,"tinyletter":"","domain":"blog.gillchristian.xyz","webhook_url":"","curated":true,"payment_platform":null,"is_premium":true,"total_notes":30},"body":"

Generic discriminated union narrowing

\n\n

\"organized

\n\n
\n\n

This post was originally a note on my Digital Garden 🌱

\n
\n\n

When working with discriminated unions, also known as tagged unions, in TypeScript one often has to narrow down a value of the union, to the type of one of the members.

\n
type Creditcard = {tag: 'Creditcard'; last4: string}\n\ntype PayPal = {tag: 'Paypal'; email: string}\n\ntype PaymentMethod = Creditcard | PayPal\n
\n

Given the above type, we would want to either check that a value is of PayPal type to show the user's email or Creditcard type to show the last 4 digits.

\n\n

This can be do simply but checking the discriminant property, in this case tag. Since each of the two members of the PaymentMethod type have different types for the tag property, TypeScript is able to, well, discriminate between them to narrow down the type. Thus the tagged union name.

\n
declare const getPaymentMethod: () => PaymentMethod\n\nconst x = getPaymentMethod()\n\nif (x.tag === 'Paypal') {\n  console.log(\"PayPal email:\", x.email)\n}\n\nif (x.tag === 'Creditcard') {\n  console.log(\"Creditcard last 4 digits:\", x.last4)\n}\n
\n

To avoid this explicit check of the tag property you might define type predicates, or guards.

\n
const isPaypal = (x: PaymentMethod): x is PayPal => x.tag === 'Paypal'\n\nconst x = getPaymentMethod()\n\nif (isPayPal(x)) {\n  console.log(\"PayPal email:\", x.email)\n}\n
\n

The problem with such guards is they have to be defined for each member of each discriminated union type in your application. Or do they?

\n\n

If you are like me you probably wonder how to do this generically for all union types. Could this be even possible with some TypeScript type level black magic?

\n\n

The short answer is yes.

\n
const isMemberOfType = <\n  Tag extends string,\n  Union extends {tag: Tag},\n  T extends Union['tag'],\n  U extends Union & {tag: T}\n>(\n  tag: T,\n  a: Union\n): a is U => a.tag === tag\n
\n

The proper response is, what the f*ck heck hack?

\n\n

Ok, let me explain.

\n\n

We need four generic types, in practice the caller will not need to pass these, they are there with the purpose of allowing TypeScript to do its type inference business.

\n\n

Notice that for all of them we are not allowing any type but instead using extends to inform TypeScript that those generic types should meet certain conditions.

\n\n
    \n
  • Tag is the union of all the tags of Union, it should extend the base type of our tag, in this case string
  • \n
  • Union is our tagged union type, it has to be an object with a tag property of type Tag
  • \n
  • T is the specific tag we want to narrow down with, and thus it should extend the Tag type
  • \n
  • And U is the specific member of Union we want to narrow down to, it should extend the Union and its tag should be T, the specific one we want
  • \n
\n\n

Tag and Union are there to define the union type we want to narrow down from and T and U, for the lack laziness of finding better names, are where the magic happens since they are the narrowed down types we expect to get to.

\n
const x = getPaymentMethod()\n\nif (isMemberOfType('Creditcard', x)) {\n  console.log('Creditcard last 4 digits:', x.last4)\n}\n\nif (isMemberOfType('Paypal', x)) {\n  console.log('PayPal email:', x.email)\n}\n
\n

Et voilà, it works!

\n\n

\"inferred

\n\n

And this is how TypeScript fills in the holes of the generics.

\n\n

\"inferred

\n\n

There's a variation of isMemberOfType that I like to use which works as a sort of getter instead of type predicate.

\n
const getMemberOfType = <\n  Tag extends string,\n  Union extends {tag: Tag},\n  T extends Union['tag'],\n  U extends Union & {tag: T}\n>(\n  tag: T,\n  a: Union\n): U | undefined =>\n  isMemberOfType<Tag, Union, T, U>(tag, a) ? a : undefined\n
\n

The usage is similar, but it requires a null check, of course.

\n
const cc = getMemberOfType('Creditcard', getPaymentMethod())\n\nif (cc) {\n  console.log('Creditcard last 4 digits:', cc.last4)\n}\n\nconst pp = getMemberOfType('Paypal', getPaymentMethod())\n\nif (pp) {\n  console.log('PayPal email:', pp.email)\n}\n
\n

There’s a little problem, the inferred type it returns is not so nice, since it's based on our definition of the generic (U extends Union & {tag: T}).

\n\n

\"inferred

\n\n

In practice this is not a problem, but since we got this far, we can keep going, right?

\n\n

Enter Extract<Type, Union>, one of TypeScript's type utilities:

\n\n
\n\n

Constructs a type by extracting from Type all union members that are assignable to Union.

\n
\n\n

For example, in the following case T0 will be of type 'a':

\n
type T0 = Extract<'a' | 'b' | 'c', 'a' | 'f'>\n
\n

The definition is simple:

\n
// Extract from Type those types that are assignable to Union\ntype Extract<Type, Union> = Type extends Union ? Type : never\n
\n

\"slaps

\n\n

With our current definition of isMemberOfType and getMemberOfType, the returned type extends union: U extends Union & {tag: T}.

\n\n

In the case of PayPal it would be PayPal & { tag: 'PayPal' }. By adding Extract to the returned type we can get PayPal instead.

\n
const isMemberOfType = <\n  Tag extends string,\n  Union extends {tag: Tag},\n  T extends Union['tag'],\n  U extends Union & {tag: T}\n>(\n  tag: T,\n  a: Union\n): a is Extract<Union, U> => a.tag === tag\n\nconst getMemberOfType = <\n  Tag extends string,\n  Union extends {tag: Tag},\n  T extends Union['tag'],\n  U extends Union & {tag: T}\n>(\n  tag: T,\n  a: Union\n): Extract<Union, U> | undefined =>\n  isMemberOfType<Tag, Union, T, U>(tag, a) ? a : undefined\n
\n

\"better

\n\n

Much cleaner this way! Now I can sleep in peace …

\n\n

In conclusion, we went from simple discriminant property check to a monstrosity of generics and type inference that achieves the same thing. Should you use these utilities in production? Of course! Even more so if it will confuse our colleagues. Maybe not, but it was fun to discover some of the cool stuff we can achieve with TypeScript.

\n\n

Here's the final version of the examples in the TypeScript Playground.

\n","links":{}},"__N_SSG":true}