HTWiki🌔

guideline


#Guideline

My guideline for writing TS types.

#Essential building blocks

These are features of TS you should know:

#Primitive types

  • boolean
  • number / bigint
  • string
  • unique symbol
  • void / undefined
  • null
  • unknown
  • never
  • any

#"Structural" types

Structural type is not a formal term. It means type from composition of multiple types.

  • product
  • union T | U
  • intersection T & U
  • function (x: T) => U

#Type Operations

  • index keyof T: object to union
  • lookup T[K]: tuple to union
  • mapped {[Key in K]: X}: map object / array
  • conditional T extends U ? X : Y: if else

#Subtype

Conditional type (if else, type constraints, pattern matching) heavily depends on subtype.

type IsSubtype<T, U> = T extends U ? true : false;

Mental model:

declare const t: T;
// U has wider range of possibility, includes T as one possibility.
const u: U = t; // ok

declare const u: U;
const t: T = u; // error

Common subtypes:

IsSubtype<bigint, number> // false
IsSubtype<void, undefined> // false
IsSubtype<undefined, void> // true
IsSubtype<string, any> // true, string is everything union
IsSubtype<any, string> // boolean, distributive
IsSubtype<unknown, string> // false
IsSubtype<string, unknown> // true!! top type, everything is assignable to unknown by design
IsSubtype<string, never> // false
IsSubtype<never, string> // never!! distribute nothing, thus never
IsSubtype<1, 1|2> // true
IsSubtype<1|2, 1> // boolean, distributive
IsSubtype<[1], [1|2]> // true
IsSubtype<[1|2], [1]> // false
IsSubtype<[1, 2], [1]> // false
IsSubtype<[1], [1, 2]> // false
IsSubtype<{a: 1}, {}> // true
IsSubtype<{}, {a: 1}> // false
IsSubtype<{a: number}, {a: 1}> // false
IsSubtype<{a: 1}, {a: number}> // true
IsSubtype<((x: number) => 1), (x: 1) => number> // true, covariance and contravariance
IsSubtype<((x: 1) => 2)&((x: '1') => '2'), (x: 1) => 2> // true, function overload
IsSubtype<((x: 1) => 2)&((x: '1') => '2'), (x: '1') => '2'> // true, function overload

#Type conversions

Possibly the most practical part.

Writing TS type is about converting between type (primitive or composed) with type operations.

Check out [[type-challenges]] for applications.

#string | number | bigint | boolean | null | undefined to string

type Allowed = string | number | bigint | boolean | null | undefined;
type NumberToString<T extends Allowed> = `${T}`;
type StringOnly<T> = T extends Allowed ? `${T}` : never;
  • template literal types
  • conditional

#object to union

type ObjectToUnion<T> = keyof T;
  • index

#tuple to union

type TupleToUnion<T extends unknown[]> = T[number];
  • generic constraints
  • lookup

#tuple to object

type TupleToObject<T extends readonly PropertyKey[]> = {
  [K in T[number]]: unknown;
}
  • generic constraints
  • mapped
  • lookup

#union to intersection

type Dist<T> = T extends T ? (x: T) => void : never;
type OverloadContra<T> = [T] extends [(x: infer X) => unknown] ? X : never;
type UnionToIntersection<U> = OverloadContra<Dist<U>>;
// X|Y
// Dist<X|Y> = (x: X) => void | (x: Y) => void;
// UnionToIntersection<X|Y> = X&Y
  • conditional
    • distributive: Dist
    • contravariance: Contra

#union to tuple

type LastInUnion<U> = Contra<UnionToIntersection<Dist<U>>>;
type UnionToTuple<U, Last = LastInUnion<U>> = [U] extends [never]
  ? []
  : [...UnionToTuple<Exclude<U, Last>>, Last];
type Fns = ((a: string) => number) | ((a: number) => string);
type OverloadFns = UnionToIntersection<Fns>;
// similar to
interface OverloadFns {
  (a: string): number;
  (a: number): string;
}
declare const f: OverloadFns;
f(1); // string
f('1'); // number
  • funciton overload LastInUnion
  • is never

#object to tuple

type ObjectToTuple<T> = UnionToTuple<ObjectToUnion<T>>;

#object to intersection

type ObjectToIntersection<T> = UnionToIntersection<ObjectToUnion<T>>;

#union to object

type UnionToObject<T> = TupleToObject<UnionToTuple<T>>;

#tuple to intersection

type TupleToIntersection<T> = UnionToIntersection<TupleToUnion<T>>;

#Miscs


EOF