as const, satisfies and type assertions
Keeping literal types, checking a value without widening it, and why `as` is a promise rather than a conversion.
After this lesson you can
- Predict when a literal widens to string or number
- Derive a union type from an as-const array or object
- Choose between an annotation, satisfies and as for a given value
- Explain why a type assertion never validates data
Widening
TypeScript infers the narrowest type it can keep true. A const string
can never change, so it keeps its literal type; a let, or an object
property, could be reassigned, so it widens:
const a = "GET"; // type "GET"
let b = "GET"; // type string
const req = { method: "GET" }; // { method: string }
That last one is why fetchWith(req) fails against a parameter typed
{ method: "GET" | "POST" }: string is wider than the union.
as const
as const tells the compiler to infer the narrowest type for the whole
expression: every literal stays a literal, every property becomes
readonly, and an array becomes a readonly tuple. From such an array you
can derive a union type and keep a single source of truth for both the
runtime list and the type:
const ROLES = ["viewer", "editor", "admin"] as const; // The element type of the tuple: "viewer" | "editor" | "admin".type Role = (typeof ROLES)[number]; function isRole(value: string): value is Role { return (ROLES as readonly string[]).includes(value);} function describe(input: string): string { return isRole(input) ? `${input} is role #${ROLES.indexOf(input) + 1}` : `${input} is not a role`;} export function run(): string[] { return ["editor", "owner"].map(describe);}It is still a compile-time marker. The array is an ordinary, mutable JavaScript array at runtime.
satisfies
An annotation replaces the inferred type with the one you wrote.
satisfies (TypeScript 4.9 and later) checks the value against a type
and keeps the inferred one. The difference shows up as soon as you read
the value back:
type Route = `/${string}`; // Annotated: the variable's type IS Record<string, Route>. The keys are forgotten.const annotated: Record<string, Route> = { home: "/", profile: "/me",}; // satisfies: checked against the same type, but the inferred type keeps its keys.const checked = { home: "/", profile: "/me",} satisfies Record<string, Route>; export function run(): unknown[] { const typo = annotated.hoem; // compiles: any string key is allowed // const typo2 = checked.hoem; // tsc: Property 'hoem' does not exist return [String(typo), checked.profile, Object.keys(checked)];}With the annotation, the variable's type is Record<string, Route>, so
annotated.hoem (a typo) compiles and is undefined at runtime. With
satisfies, the compiler still checks every value is a Route, but the
type keeps the exact keys, so checked.hoem is a compile error.
as, the assertion
value as T says "treat this as a T". The compiler refuses only an
assertion between types that cannot overlap ("x" as number is an
error); otherwise it allows it, and from then on it believes you. Nothing is
converted and nothing is checked at runtime:
interface Config { port: number; host: string;} export function run(): unknown[] { // as is a promise you make to the compiler. Nothing checks it. const fromEnv = JSON.parse('{"port":"8080"}') as Config; const empty = {} as Config; return [fromEnv.port + 1, typeof fromEnv.host, String(empty.port)];}x as unknown as T forces any assertion through, which is a sign the claim
needs a runtime check instead.
Common mistakes
- Using
asto "parse".JSON.parse(text) as Configis a promise, not validation: the port above is still the string"8080", and+ 1makes"80801". - Annotating a config object with a wide type.
Record<string, X>forgets which keys exist, so typos compile. Usesatisfiesto check the values and keep the keys. - Mutating an
as constarray through a cast. It works at runtime; the readonly-ness was only in the type. - Searching an
as consttuple with a plain string.ROLES.indexOf(x)wants aRole; widen the array toreadonly string[]for the search rather than assertingx as Role— and rememberindexOfreturns-1for a miss.
What interviewers ask
Exercise · Pro
Every role at or above a minimum
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade