Type assertions

Sometimes you know more than TypeScript does. For example, document.getElementById returns HTMLElement | null, but you know the element is a canvas:

const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;

Assertions are erased at compile time : no runtime check. If you are wrong, you can get runtime errors.

Angle-bracket form is equivalent (avoid in .tsx files):

const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");

TypeScript only allows assertions between related types. Impossible coercions are blocked:

const x = "hello" as number;
// Error: Conversion of type 'string' to type 'number' may be a mistake...

Force it only through unknown or any when you truly mean it:

const a = expr as unknown as T;

Literal types

You can use specific strings and numbers as types:

let x: "hello" = "hello";
x = "hello"; // OK
x = "howdy"; // Error

Alone that is rarely useful. Combined into a union, literals model allowed values:

function printText(s: string, alignment: "left" | "right" | "center") {
  // ...
}

printText("Hello, world", "left");
printText("G'day, mate", "centre");
// Error: Argument of type '"centre"' is not assignable to parameter of type '"left" | "right" | "center"'.

Numeric literals work too:

function compare(a: string, b: string): -1 | 0 | 1 {
  return a === b ? 0 : a > b ? 1 : -1;
}

Mix with object types:

interface Options {
  width: number;
}

function configure(x: Options | "auto") {
  // ...
}

configure({ width: 100 });
configure("auto");

boolean is essentially the union true | false.

Literal inference and as const

Object properties are often widened to string / number:

declare function handleRequest(url: string, method: "GET" | "POST"): void;

const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method);
// Error: Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.

Fixes:

const req = { url: "https://example.com", method: "GET" as "GET" };
// or
handleRequest(req.url, req.method as "GET");
// or freeze the whole object as literals:
const req2 = { url: "https://example.com", method: "GET" } as const;
handleRequest(req2.url, req2.method);

as const makes TypeScript treat properties as their literal types.

null and undefined

JavaScript uses both for "missing" values. With strictNullChecks on (recommended), you must handle them:

function doSomething(x: string | null) {
  if (x === null) {
    // handle missing value
  } else {
    console.log("Hello, " + x.toUpperCase());
  }
}

Non-null assertion (!)

! asserts a value is not null or undefined:

function liveDangerously(x?: number | null) {
  console.log(x!.toFixed());
}

Same rules as other assertions: no runtime check. Prefer real narrowing when you can.

Enums and other primitives (brief)

  • Enums : TypeScript feature for named constants; useful to know about, but often string literal unions are enough.
  • bigint : large integers (100n or BigInt(100)).
  • symbol : unique references via Symbol("name").

Practice

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";

function request(url: string, method: HttpMethod) {
  console.log(method, url);
}

const options = { url: "/api/users", method: "GET" } as const;
request(options.url, options.method);

function lengthOf(s: string | null | undefined) {
  // return s.length safely
}

Implement lengthOf with narrowing (no ! unless you can justify it).