Type Assertions and Modifiers

This section documents the current TypeScript release line as published at the official TypeScript documentation, which is the reference these pages are written and verified against. No specific patch version is pinned.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since TypeScript iterates quickly.

This section’s bibliography lists the reference material consulted while preparing these pages.

Assertions and modifiers tell the compiler something it cannot infer on its own, or ask it to check a value without changing the type it infers. They sit on top of the inference rules in The Type System and the control-flow narrowing in Unions and Narrowing; reach for them only when inference genuinely falls short.

The as assertion

An as assertion changes the static type the compiler uses for an expression. It emits no runtime code and performs no check — if you are wrong, the failure surfaces later.

const el = document.getElementById("main") as HTMLCanvasElement;
const ctx = el.getContext("2d");   // ok: el is treated as HTMLCanvasElement

Assertions cannot lie too much

An assertion may only move between related types — one must be assignable to the other, or they must share enough structure. A nonsense assertion is rejected outright:

const n = 42 as string;
//        ~~~~~~~~~~~~~~
// Conversion of type 'number' to type 'string' may be a mistake because
// neither type sufficiently overlaps with the other.

The Type Assertions handbook section describes this as the rule that an assertion may only add or remove a "relatively specific" slice of a type.

The two-step as unknown as T

When you must cross genuinely unrelated types, route through unknown — assignable from anything, and (via the second assertion) to anything:

const wire = '{"id":1}' as unknown as { id: number };

Requiring two assertions is a deliberate speed bump: it makes the unsafe step obvious in review. Prefer a real parse-and-validate step instead.

Asserting the type of a caught error

Under useUnknownInCatchVariables (on with strict), the binding in catch (e) has type unknown. Narrow it — do not blind-cast it:

try {
  risky();
} catch (e) {
  if (e instanceof Error) {
    console.error(e.message);   // narrowed, safe
  } else {
    console.error(String(e));
  }
}

Non-null and definite-assignment assertions

The postfix ! (non-null assertion) removes null and undefined from an expression’s type, with no runtime check:

function firstChar(s?: string) {
  return s!.charAt(0);   // asserting s is defined here
}

The form on a declaration, !: (definite-assignment assertion), states that a field or variable is assigned before use even though the compiler cannot prove it — common with dependency injection or test setup:

class Widget {
  private service!: DataService;   // set in an init hook, not the constructor

  init(s: DataService) { this.service = s; }
}

let config!: Config;
setup();          // assigns config
config.load();    // no "used before being assigned" error

Both are unchecked promises. If you can restructure so the value is provably present — a constructor parameter, an early return — do that instead.

Assertion functions

An assertion function narrows its argument for the rest of the calling scope by throwing when a condition fails. Its return annotation is asserts x is T, or the bare asserts condition form:

function assert(condition: unknown, msg?: string): asserts condition {
  if (!condition) throw new Error(msg);
}

function assertString(v: unknown): asserts v is string {
  if (typeof v !== "string") throw new Error("not a string");
}

function use(v: unknown) {
  assertString(v);
  v.toUpperCase();   // v is string from here on
}

See Assertion Functions for the signature rules — an explicit return-type annotation is mandatory, and the annotation is lost on a function expression assigned without one.

const assertions

as const asserts an expression to its narrowest, deeply immutable form:

  • primitive literals keep their literal type ("GET", not string);

  • every property becomes readonly, recursively;

  • array literals become readonly tuples rather than T[] — tuple freezing.

const method = "GET" as const;          // type "GET"

const route = { path: "/api", method: "GET" } as const;
// { readonly path: "/api"; readonly method: "GET" }

const pair = [1, "a"] as const;         // readonly [1, "a"]

const methods = ["GET", "POST", "PUT"] as const;
type Method = (typeof methods)[number]; // "GET" | "POST" | "PUT"

This is the idiomatic way to derive a union type from a runtime list, and to stop an object literal from widening when it is passed to a stricter parameter (see Everyday Types).

The satisfies operator

satisfies checks that a value is assignable to a type without widening it — the compiler keeps the precise type it inferred for the value. It is the modern replacement for many annotation / as pairs.

type Config = Record<string, string | number>;

// (a) annotation -- checked, but the specific keys and value types are lost
const a: Config = { host: "localhost", port: 8080 };
a.host.toUpperCase();
//     ~~~~~~~~~~~ Error: 'string | number' has no method 'toUpperCase'

// (b) as -- keeps the shape, but silences real mistakes
const b = { host: "localhost", prot: 8080 } as Config;   // typo NOT caught

// (c) satisfies -- validated against Config AND keeps the literal types
const c = { host: "localhost", port: 8080 } satisfies Config;
c.host.toUpperCase();   // ok: c.host is string
c.port.toFixed();       // ok: c.port is number
// { host: "localhost", prot: 8080 } satisfies Config -> Error: 'prot' is unknown

satisfies arrived in TypeScript 4.9; the satisfies operator release note walks through the colour-palette example it was designed for. The two forms combine — …​ as const satisfies T gives literal narrowing and a type check.

any, unknown and never

unknown is the top type (every value is one), never is the bottom type (no value is one), and any steps outside the type system in both directions.

Assignability arrows between any, unknown, never and every other type

unknown

Accepts any value; assignable to nothing until you narrow it. The safe landing type for JSON.parse, catch, and other untyped boundaries.

any

Assignable both to and from everything, with all checking disabled. Contagious — one any spreads through every expression that touches it.

never

Assignable to every type, but nothing is assignable to it. Marks an unreachable branch or a function that never returns; drives exhaustiveness checks.

Keeping any at the narrowest scope

When any is unavoidable, confine it and pick the most precise variant:

// Prefer these over a bare `any`:
let bag: any[];                          // an array of something
type Handler = (...args: any[]) => any;  // some callable

// Hide the unavoidable unsafe assertion inside a well-typed function:
function getConfigValue<T>(key: string): T {
  return (globalThis as any).__CONFIG__[key] as T;   // one cast, one place
}
const port = getConfigValue<number>("port");   // callers see a clean signature

The unsafe step lives in one reviewed function; every call site stays fully typed.

Tracking type coverage

strict does not ban any, and implicit any still leaks in from untyped dependencies. Enabling noImplicitAny (part of strict) catches the accidental cases at compile time. For the explicit ones, the type-coverage tool reports the share of identifiers typed more precisely than any, so you can ratchet it upward in CI:

npx type-coverage --detail --at-least 99