Skip to content

karlhorky/typescript-tricks

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 

Repository files navigation

TypeScript Tricks

A collection of useful TypeScript tricks

DeepImmutable aka DeepReadonly Generic

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]>
}

Empty Object Type

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'

Playground

JSON.stringify() an Object with Regular Expression Values

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\\\\/[^/]+$/"}'

Opaque Generic

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:

type Opaque<K, T> = T & { __TYPE__: K };

Prettify Generic

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;

Spread Generic

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>;

Related

For higher quality utility types, you may have better luck with:

About

A collection of useful TypeScript tricks

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published