Typescript theory

💻Programming Language
typescript
theory

Type hierarchy

Explained in this article about unknown and never:

  • Top type unknown
  • Bottom type never
  • Top & Bottom type any

See this SO answer for more.

T | neverT
T & unknownT
  • never is the identity with respect to unions (T | never ⇒ T)
  • unknown is the identity with respect to intersections (T & unknown ⇒ T)

(but number & string is of type never)

Difference between any and unknown

unknown is I don’t know; any is I don’t care

Taken from this StackOverFlow thread about the difference between any and unknown

TypeScript Type Variance

  • TypeScript is covariant in shapes (example)
  • TypeScript is structurally typed
interface Animal {
  name: string;
}

// This works also if Bird does not extend Animal,
// but just gets defined as { name: string; wingCount: number; }
interface Bird extends Animal {
  wingCount: number;
}

const myAnimal: Animal = { name: "snake" };
const myBird: Bird = { name: "pigeon", wingCount: 2 };

const getName = (obj: Animal) => obj.name;

// TypeScript is covariant in shapes
// 'Bird' allowed although 'Animal' required
getName(myBird);

Function types and contravariance

function types are covariant in their return type but contravariant in their parameter types

interface Foo {
  name: string;
}

interface Bar {
  name: string;
  displayName: string;
}


function getName(obj: Foo): '1' | '2' {
  return "1";
}

const getName2: (obj: Bar) => string = getName;

Here the parameter of getName2 is a subtype of the parameter of getName but the return type is a supertype of the return type of getName.

Corresponding TS Playground

Covariance & Contravariance (again)

In general

  • TypeScript shapes are covariant in their property types.
  • Every complex type is covariant in its members and function return types.
  • Functions are covariant in their return types (the return type has to be a subtype of the other function’s return type)
  • Functions are contravariant in their parameter and this types.

    • In other words: For a function to be a subtype of another function, each of its parameters and its this type must be a supertype of its corresponding parameter in the other function.
const test = (arg: "a" | "b"): "a" | "b" | "c" => {
    return "c";
};

const test2 = (arg: "a" | "b" | "c"): "a" | "b" | "c" | "d" => {
    return "c";
};

const useTest = (arg: typeof test) => {
    //
}
// Error: Since TS is contravariant on function parameter values but not on function return values this throws a compile error
useTest(test2);

See this TS Playground

See also this discussion I had in StackOverFlow comments about Variance and Covariance

Assignability and Type Annotation

Assignability: TypeScript’s rules for whether or not you can use a type A where another type B is required.

If type A extends type B then type A is assignable to type B. You can say that

Example

type Book = { name: string; isbn: string; author?: string }

let Book: Book = { name: 'Test' }

Examples:

  • "hello" extends string
  • string extends number | string
  • nothing extends never
  • everything (besides never) extends unknown

Type Assertion

If type B extends type A then type A is asserted to be type B

Top and bottom types

  • unknown is a top type - the root of the hierarchy.

    • Any value is assignable to something typed as unknown (e.g. let a: unknown = 5; is possible, but let a: unknown = 5; let b = 6; b = a; is not) (because everything inherits from it)
    • This e.g. works: let Book: unknown = { name: 'Test' }
    • But elements with type unknown are assignable to nothing else (because you inherit from nothing)
    • This doesn’t work: let Sth: unknown; const New: Book = Sth;. You e.g. get “Type ‘unknown’ is not assignable to type ‘Book’”
    • unknown is an empty type: It has no properties of its own.
  • never is a bottom type

    • Nothing is assignable to elements with type never
    • This e.g. doesn’t work: const Book: never = { name: 'Test' };. You get “Type ‘string’ is not assignable to type ‘never’.”
    • But the never type is assignable to everything.

Usages of never

Be safe against future changes to types, e.g. change from

// former type
const stringOrNum = string | number = 'Hello world!';
// new type
const stringOrNum = string | number | boolean = 'Hello world!';

Exhaustive check: TypeScript would throw a compile error.

if (typeof stringOrNum === 'string') {
  // ...
} else if (typeof stringOrNum === 'number') {
  // ...
} else {
  const _exhaustiveCheck: never = stringOrNum
  throw new Error(`Unknwon type ${_exhaustiveCheck}`)
}

any

  • It’s both, a bottom and a top type.

    • It’s assignable to everything
    • Everything is assignable to it.

unkown vs. any

unknown is I don’t know; any is I don’t care.

taken from here

Type Widening and Narrowing

In case of a type assertion, e.g. const B: foo as A it will be allowed if A extends B or if B extends A.

  • If A extends B you widen A to B which is relatively safe and acts similarly to a type annotation.

    Example: string extends number | string

  • If B extends A you narrow A to B which is unsafe.

    Example: number | string extends string

Type widening

Type widening is the default behavior for many literal types. So e.g. a string foo becomes the string type.

  • If you widen A to B, A extends B holds. e.g. when doing a type assertion let foo = bar as A where bar is of a type which “extends” B, i.e. is a superset of A.

Marius Schulz writes why non-widening literal types might be useful.

Control flow analysis

Discuss on TwitterImprove this article: Edit on GitHub

Discussion


Explain Programming

André Kovac builds products, creates software, teaches coding, communicates science and speaks at events.