623K lines into our TypeScript journey, we’ve run into enough problems with resorting to any
that we’ve put in the extra effort to type the harder stuff at the core of our platform. Read on for some of the more exotic types we’ve discovered and why you might want to use them in your own projects!
Json
If it type checks, you can serialize it. We use this type as a constraint anywhere we serialize JSON over the wire, whether it’s a background task payload, a pubsub message, or metadata we store in Postgres.
type Json =
| undefined
| null
| boolean
| number
| string
| Json[]
| { [key: string]: Json };
See the improved support for recursive type aliases in TypeScript 3.7 that made this possible.
DeepPartial
For all your stubbing needs. We use this frequently in tests where we only need partial objects but want type safety for nested types!
export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends Array<infer U>
? Array<DeepPartial<U>>
: T[P] extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: DeepPartial<T[P]> | T[P];
};
Credit: TypeORM.
DeepReadonly
Zero-cost immutability.
export type DeepReadonly<T> = T extends Function
? T
: T extends ReadonlyArray<infer R>
? ReadonlyArray<DeepReadonly<R>>
: T extends Map<infer K, infer V>
? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
: T extends object
? { readonly [P in keyof T]: DeepReadonly<T[P]> }
: T;
Credit: https://github.com/microsoft/TypeScript/issues/13923#issuecomment-488160343
Paths
This type lets us create a clean DX for certain nested keys.
/**
* This type takes an object `T` and traverses `D` paths deep to create a full dot-notation map of its keys.
* The depth here is chosen arbitrarily to avoid downstream warnings
* where TS detects possible infinite-depth type instantiation.
*/
type Paths<T, D extends number = 6> = [D] extends [never]
? never
: T extends Array<infer ArrayType>
? "0" | Join<"0", Paths<ArrayType, Prev[D]>>
: T extends object
? {
[K in keyof T]-?: K extends string | number
? `${K}` | Join<K, Paths<T[K], Prev[D]>>
: never;
}[keyof T]
: "";
/**
* Joins key K to path P. E.g., `Join<"mama", "yo"> = "yo.mama"`
*/
export type Join<K, P> = K extends string | number
? P extends string | number
? `${K}${"" extends P ? "" : "."}${P}`
: never
: never;
/**
* When doing depth-based types (e.g., Paths, below), TS will go infinitely
* deep if you let, leading to errors. This technique prevents that by making
* sure TS only goes `n` levels deep, where `n` is the number of elements in
* the below tuple.
*/
export type Prev = [never, 0, 1, 2, 3, 4, 5, 6, ...0[]];
And it works like this:
// type Test = "foo" | "foo.bar" | "foo.bar.baz"
type Test = Paths<{
foo: {
bar: {
baz: number;
};
};
}>;
Camelize
and friendsFor making APIs with different naming conventions play nice together. We turn everything into camelCase
in our API gateway for consistency, and now we can do that in a type-safe way.
type CamelKey<T extends string | number | symbol> =
T extends `${infer A}_${infer B}`
? CamelKey<`${A}${capitalize(B)}`>
: T extends string ? `${uncapitalize(T)}` : T;
type Camelize<T> =
T extends Record<string | number | symbol, unknown>
? { [K in keyof T as CamelKey<K>]: Camelize<T[K]> }
: T;
And it works like this:
type Result = Camelize<{
my_profile: {
created_at: "2020-01-01";
is_active: false;
};
Weird_API: {
HasWeirdFields: {
123: 456;
true: false;
};
};
}>;
// type Result = {
// myProfile: {
// createdAt: "2020-01-01";
// isActive: false;
// };
// weirdAPI: {
// hasWeirdFields: {
// 123: 456;
// true: false;
// };
// };
// }
Works for PascalCase
too!
MultipleParameters
For inferring signatures of overloaded functions.
The built-in Parameters
type has a surprising and unintuitive limitation:
function isTruthy(value: boolean): boolean;
function isTruthy(value: string): string;
function isTruthy(value: any) {
return Boolean(value);
}
// Incorrect!
type Test = Parameters<typeof isTruthy>; // [value: string]
As of TypeScript 4.4, the only known way to infer multiple signatures is to infer a signature for each number of compatible overrides:
type MultipleParameters<T extends Fn> = T extends {
(...args: infer A1): any;
(...args: infer A2): any;
(...args: infer A3): any;
}
? A1 | A2 | A3
: T extends { (...args: infer A1): any; (...args: infer A2): any }
? A1 | A2
: T extends { (...args: infer A1): any }
? A1
: never;
type Fn<TArgs extends any[] = any[]> = (...args: TArgs) => any;
Where would we ever use this inference? On to the next item!
An interface for calling a generic function with deferred arguments.
Types are correctly inferred for up to 3 overloaded signatures of fn
, but you may need to pass a custom callback with narrower argument types if TSC gives you a type error for something you expected to check.
export interface Thunk {
<T extends Fn>(fn: T, ...args: MultipleParameters<T>): () => ReturnType<T>;
}
type Fn<TArgs extends any[] = any[]> = (...args: TArgs) => any;
In use:
const toThunk: Thunk =
(fn, ...args) =>
() =>
fn(...args);
UnionToIntersection
You have a union. You want the intersection of all the unioned types. Just read this excellent post:
https://fettblog.eu/typescript-union-to-intersection/
Resolvable
Say you have a parent type in GraphQL that has some specific data that’s useful for resolving one its subfields. This keeps you from easily splitting the resolver out because you lose access to the key input data!
To make things more complicated, this field can be expensive, so you want to make sure it’s resolved lazily when a consumer really needs it.
TL;DR, if you’re using Apollo server it allows you to safely resolve callbacks as GraphQL field-level resolvers — inline with the parent level resolver.
type Resolvable<T> = {
[K in keyof T]: T[K] extends object
?
| T[K]
| (() =>
| T[K]
| Promise<T[K]>
| Resolvable<T[K]>
| Promise<Resolvable<T[K]>>)
| Resolvable<T[K]>
: T[K] | (() => T[K] | Promise<T[K]>);
};
We hook it into our GraphQL codegen like so:
config:
mappers:
GoalPermissions: Resolvable<GoalPermissions>
ExactlyOne
The Highlander of types.
/**
* Given a record T, only allow instances of Partial<T> with a single key.
*
* @example
*
* // Yields exactly `{ foo: string }`
* ExactlyOne<{ foo: string; bar: number }, "foo">
*
* @example
*
* // Yields `never`
* ExactlyOne<{ foo: string; bar: number }, "foo" | "bar">
*/
export type ExactlyOne<T, K extends keyof T> = Exactly<
Singles<keyof T>,
Pick<T, K>
>;
type Singles<T extends PropertyKey> = T extends infer U
? { [P in U extends PropertyKey ? U : never]: unknown }
: never;
type Exactly<T, P> = T extends {}
? P & { [K in Exclude<keyof P, keyof T>]?: never }
: never;