A collection of useful TypeScript tricks
Deep immutable (readonly) generic type for specifying multi-level data structures that cannot be modified.
Example:
let deepX: DeepImmutable<{y: {a: number}}> = {y: {a: 1}};
deepX.y.a = 2; // Fails as expected!
Credit: @nieltg in Microsoft/TypeScript#13923 (comment)
type Primitive = undefined | null | boolean | string | number | Function
type Immutable<T> =
T extends Primitive ? T :
T extends Array<infer U> ? ReadonlyArray<U> :
T extends Map<infer K, infer V> ? ReadonlyMap<K, V> : Readonly<T>
type DeepImmutable<T> =
T extends Primitive ? T :
T extends Array<infer U> ? DeepImmutableArray<U> :
T extends Map<infer K, infer V> ? DeepImmutableMap<K, V> : DeepImmutableObject<T>
interface DeepImmutableArray<T> extends ReadonlyArray<DeepImmutable<T>> {}
interface DeepImmutableMap<K, V> extends ReadonlyMap<DeepImmutable<K>, DeepImmutable<V>> {}
type DeepImmutableObject<T> = {
readonly [K in keyof T]: DeepImmutable<T[K]>
}
To verify that an object has no keys, use Record<string, never>
:
type EmptyObject = Record<string, never>;
const a: EmptyObject = {}; // ✅
const b: EmptyObject = { z : 'z' }; // ❌ Type 'string' is not assignable to type 'never'
JSON.stringify()
on an object with regular expressions as values will behave in an unual way:
JSON.stringify({
name: 'update',
urlRegex: /^\/cohorts\/[^/]+$/,
})
// '{"name":"update","urlRegex":{}}'
Use a custom replacer function to call .toString()
on the RegExp:
export function stringifyObjectWithRegexValues(obj: Record<string, unknown>) {
return JSON.stringify(obj, (key, value) => {
if (value instanceof RegExp) {
return value.toString();
}
return value;
});
}
This will return a visible representation of the regular expression:
stringifyObjectWithRegexValues({
name: 'update',
urlRegex: /^\/cohorts\/[^/]+$/,
})
// '{"name":"update","urlRegex":"/^\\\\/cohorts\\\\/[^/]+$/"}'
A generic type that allows for checking based on the name of the type ("opaque" type checking) as opposed to the data type ("transparent", the default in TypeScript).
Example:
type Username = Opaque<"Username", string>;
type Password = Opaque<"Password", string>;
function createUser(username: Username, password: Password) {}
const getUsername = () => getFormInput('username') as Username;
const getPassword = () => getFormInput('password') as Password;
createUser(
getUsername(),
getUsername(), // Error: Argument of type 'Opaque<"Username", string>' is not assignable to
// parameter of type 'Opaque<"Password", string>'.
);
Credit:
- @stereobooster in Pragmatic types: opaque types and how they could have saved Mars Climate Orbiter
- @phpnode in Stronger JavaScript with Opaque Types
type Opaque<K, T> = T & { __TYPE__: K };
A generic type that shows the final "resolved" type without indirection or abstraction.
const users = [
{ id: 1, name: "Jane" },
{ id: 2, name: "John" },
] as const;
type User = (typeof users)[number];
type LiteralToBase<T> = T extends string
? string
: T extends number
? number
: T extends boolean
? boolean
: T extends null
? null
: T extends undefined
? undefined
: T extends bigint
? bigint
: T extends symbol
? symbol
: T extends object
? object
: never;
type Widen<T> = {
[K in keyof T]: T[K] extends infer U ? LiteralToBase<U> : never;
};
export type Prettify<Type> = Type extends {}
? Type extends infer Obj
? Type extends Date
? Date
: { [Key in keyof Obj]: Prettify<Obj[Key]> } & {}
: never
: Type;
type WideUser = Widen<User>;
// ^? Widen<{ readonly id: 1; readonly name: "Jane"; }> | Widen<{ readonly id: 2; readonly name: "John"; }>
type PrettyWideUser = Prettify<Widen<User>>;
// ^? { readonly id: number; readonly name: string; } | { readonly id: number; readonly name: string; }
Credit:
export type Prettify<Type> = Type extends {}
? Type extends infer Obj
? Type extends Date
? Date
: { [Key in keyof Obj]: Prettify<Obj[Key]> } & {}
: never
: Type;
A generic type that allows for more soundness while using object spreads and Object.assign
.
type A = {
a: boolean;
b: number;
c: string;
};
type B = {
b: number[];
c: string[] | undefined;
d: string;
e: number | undefined;
};
type AB = Spread<A, B>;
// type AB = {
// a: boolean;
// b: number[];
// c: string | string[];
// d: string;
// e: number | undefined;
//};
Credit:
type Diff<T, U> = T extends U ? never : T; // Remove types from T that are assignable to U
// Names of properties in T with types that include undefined
type OptionalPropertyNames<T> =
{ [K in keyof T]: undefined extends T[K] ? K : never }[keyof T];
// Common properties from L and R with undefined in R[K] replaced by type in L[K]
type SpreadProperties<L, R, K extends keyof L & keyof R> =
{ [P in K]: L[P] | Diff<R[P], undefined> };
// Type of { ...L, ...R }
type Spread<L, R> =
// Properties in L that don't exist in R
& Pick<L, Diff<keyof L, keyof R>>
// Properties in R with types that exclude undefined
& Pick<R, Diff<keyof R, OptionalPropertyNames<R>>>
// Properties in R, with types that include undefined, that don't exist in L
& Pick<R, Diff<OptionalPropertyNames<R>, keyof L>>
// Properties in R, with types that include undefined, that exist in L
& SpreadProperties<L, R, OptionalPropertyNames<R> & keyof L>;
For higher quality utility types, you may have better luck with: