Variables#
Bindings work the same as JavaScript (const, let, var
plus block / function scope; see
Variables). What TypeScript
adds is inference at the binding site and narrowing
through control flow.
Inference#
The compiler infers a type at the declaration site from the initializer.
const port = 8080; // const: literal type `8080`
let port2 = 8080; // let: widened to `number`
const config = {host: "0.0.0.0", port: 80};
// ^? { host: string; port: number }
const tuple = [1, 2]; // number[], not [number, number]
const tuple2 = [1, 2] as const; // readonly [1, 2]
const keeps the literal type for primitives; let widens
to the base type because reassignment must be allowed. For
arrays and objects the operator adds as const to keep
literal precision.
Annotations#
Annotate when the inferred type would be too wide, when the operator wants to fix the type at the declaration site, or when the initializer is missing.
const handlers: Record<string, Handler> = {};
let result: Result<User, Error>;
function load(): Promise<Config> {
// ...
}
For function parameters and return types of public APIs, the operator annotates by default. The annotation freezes the contract; inference can shift if internals change.
Narrowing#
The compiler narrows a binding’s type as control flows through
checks. After typeof x === "string", the compiler treats
x as string in that branch.
function display(x: string | number) {
if (typeof x === "string") {
x.toUpperCase(); // x: string here
} else {
x.toFixed(2); // x: number here
}
}
The compiler also narrows on truthiness, instanceof, in,
discriminant checks on a union, and user-defined type guards.
type Shape = {kind: "circle"; r: number} | {kind: "rect"; w: number; h: number};
function area(s: Shape): number {
switch (s.kind) {
case "circle": return Math.PI * s.r ** 2;
case "rect": return s.w * s.h;
}
}
const vs let#
Pick const by default; let only when the
binding genuinely changes. The type system gains precision when
the operator commits to const.
const status = "ok"; // status: "ok" (literal)
let status2 = "ok"; // status2: string (widened)
if (Math.random() > 0.5) {
status2 = "fail"; // allowed because let
}
const assertion#
as const freezes the binding and every nested member. Useful
for option objects and lookup tables.
const COLORS = ["red", "green", "blue"] as const;
type Color = (typeof COLORS)[number]; // "red" | "green" | "blue"
const ROLES = {admin: 0, user: 1, guest: 2} as const;
type Role = keyof typeof ROLES; // "admin" | "user" | "guest"
Destructuring with annotations#
Annotations apply to the binding as a whole, not the individual names.
const {host, port}: {host: string; port: number} = config;
const [first, ...rest]: [string, ...number[]] = list;
The clearer form is usually to annotate the source.
const config: Config = load();
const {host, port} = config; // inferred per the source
Globals and ambient#
Globals live on globalThis. The operator declares ambient
structures inside a .d.ts file so the compiler knows about them.
// env.d.ts
declare global {
namespace NodeJS {
interface ProcessEnv {
readonly DATABASE_URL: string;
readonly API_KEY?: string;
}
}
}
export {}; // make this a module