sapling/addons/shared/typeUtils.ts
Evan Krause 10139ab86b Add StrictUnion type helper
Summary:
This is a small type utility that makes it a little easier to work with data from graphql.

We get types back from graphql like:
```
type TypeaheadResult = {
  __typename: 'diff',
  diff: {
    title: string
  }
} | {
  __typename: 'task',
  task: {
    title: string,
  }
}
```

This means when looking at the results, e.g. filtering, you need to cast:
```
results.map(result => {
  return (result as typeof result & {__typename: 'diff'}).diff?.title ?? (result as typeof result & {__typename: 'task'}).task?.title;
});
```
otherwise, since __typename is not narrowed at all, both the task and diff fields are missing, as opposed to optional.

This type util converts the union into one where all the keys are required (but nullable).
So this type becomes:
```
type TypeaheadResult = {
  __typename: 'diff' | 'task',
  diff?: {
    title: string
  },
  task?: {
    title: string
  }
}
```

You still have to validate that you have the property you want, but this is easy with null coalescing:
```
results.map((result: StrictUnion<Result>) => {
  return result.diff?.title ?? result.task?.title;
});
```

Reviewed By: quark-zju

Differential Revision: D53277531

fbshipit-source-id: 005cb7681c79bf197689fd8db1a74a9cc5684e7d
2024-01-31 14:21:56 -08:00

57 lines
1.7 KiB
TypeScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* Remove particular keys from an object type:
* ```
* Without<{foo: string, bar: string, baz: number}, 'bar' | 'baz'> => {foo: string}
* ```
*/
export type Without<T, U> = {[P in Exclude<keyof T, keyof U>]?: never};
/**
* Given two object types, return a type allowing keys from either one but not both
* ```
* ExclusiveOr({foo: string}, {bar: number}) -> allows {foo: 'a'}, {bar: 1}, but not {foo: 'a', bar: 1} or {}
* ```
*/
export type ExclusiveOr<T, U> = T | U extends object
? (Without<T, U> & U) | (Without<U, T> & T)
: T | U;
/**
* Make every key of a type optional, and make its type undefined
* ```
* AllUndefined<{foo: string}> => {foo?: undefined}
* ```
*/
export type AllUndefined<T> = {[P in keyof T]?: undefined};
/**
* Make every key of the object NOT readonly. The opposite of Readonly<T>.
* ```
* {readonly foo: string} -> {foo: string}
* ```
*/
export type Writable<T> = {-readonly [P in keyof T]: T[P]};
export type Json = string | number | boolean | null | Json[] | {[key: string]: Json};
type UnionKeys<T> = T extends unknown ? keyof T : never;
type StrictUnionHelper<T, TAll> = T extends unknown
? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, undefined>>
: never;
/**
* Make a union type T be a strict union by making all keys required.
* This allows a discriminated union to have fields accessed without a cast.
* For example,
* ```
* StrictUnion<{type: 'foo', foo: string} | {type: 'bar', bar: number}> => {type: 'foo' | 'bar', foo?: string, bar?: number}
* ```
*/
export type StrictUnion<T> = StrictUnionHelper<T, T>;