Declared type

// The "declared" type is `string | number`
let x = Math.random() < 0.4 ? 10 : ""

x = 10 // therefore this is ok
x = "foobar" // and this as well

Type predicate

// A function that accepts a single argument and returns a boolean
// A predicate helps Typescript when the control flow analysis is not enough
// The predicate is like `argumentName is Type`
function isString(input: string | number): input is string {
return typeof input === "string"
}

let a = Math.random() < 0.3 ? 10 : "foobar"

if (isString(a)) {
a // Typescript now knows it is a string
} else {
a // And now a number
}

Exhaustive check with discriminated union

interface Square {
kind: "square"
size: number
}

interface Rectangle {
kind: "rectangle"
width: number
height: number
}

type Shape = Square | Rectangle

function area(shape: Shape) {
switch (shape.kind) {
case "square":
return shape.size * shape.size
case "rectangle":
return shape.width * shape.height
// If a new case is added at compile time you will get a compile error
// If a new value appears at runtime you will get a runtime error
default:
return assertNever(shape)
}
}

function assertNever(x: never): never {
throw new Error(`Should have been never. Unexpected value: ${x}`)
}

The assertion can also be a class that is extended from an Error

class UnreachableCaseError extends Error {
constructor(value: never) {
super(`Unreachable case: ${value}`);
}
}

...
default: throw new UnreachableCaseError(shape)

Or the default case can be omitted, but then there is no runtime check and the switch has to be terminated with return statements.

Resources