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!
JsonIf 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.
DeepPartialFor 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.
DeepReadonlyZero-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
PathsThis 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!
MultipleParametersFor 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);
UnionToIntersectionYou have a union. You want the intersection of all the unioned types. Just read this excellent post:
https://fettblog.eu/typescript-union-to-intersection/
ResolvableSay 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>
ExactlyOneThe 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;