Syntax#
TypeScript starts from JavaScript syntax and layers type
annotations on top: a colon after a binding or parameter, an
optional return type after a function signature, as for
assertions, ! for non-null, ? for optional members.
Everything else (statements, expressions, literals, operators,
control flow) is JavaScript; see
Syntax for the base layer.
/types`.
Annotations#
The basic annotation form is name: Type.
const count: number = 0;
let name: string = "rk";
const items: string[] = [];
const config: {host: string; port: number} = {host: "0.0.0.0", port: 80};
function add(a: number, b: number): number {
return a + b;
}
const greet = (name: string): string => `hello, ${name}`;
Annotations are optional whenever the compiler can infer the type. The operator leaves inference do its job for locals; annotates exports, public function signatures, and any binding whose inferred type would be wider than intended.
const xs = [1, 2, 3]; // inferred: number[]
const config = {host: "0.0.0.0"}; // inferred: {host: string}
Assertions#
x as T tells the compiler to treat x as T without
checking. Use this only when the compiler genuinely
cannot follow the proof; otherwise the type system loses its
guarantees.
const el = document.getElementById("root") as HTMLDivElement;
const data = JSON.parse(raw) as Config; // dangerous; use zod instead
The older <T>x syntax exists but conflicts with JSX; the
operator writes as form by default.
The non-null assertion ! is a special case: x! is x as
NonNullable<typeof x>. Useful after a runtime check the
compiler does not see.
const found = items.find(x => x.id === id);
process(found!); // operator knows it exists
const assertion (as const) narrows literal types and
makes everything readonly.
const tuple = [1, 2, 3] as const; // readonly [1, 2, 3]
const colors = ["red", "green", "blue"] as const;
// ^? readonly ["red", "green", "blue"]
type Color = (typeof colors)[number]; // "red" | "green" | "blue"
The satisfies operator (x satisfies T) checks that x
conforms to T without widening the type. Preferred over a
plain annotation when you want the inferred narrow
type and the conformance check.
const handlers = {
get: (req) => req.path,
post: (req) => req.body,
} satisfies Record<string, (req: Req) => unknown>;
handlers.get; // type is (req: Req) => string, not the wider Record value
Optional and readonly#
? marks an object property or parameter as optional;
readonly marks it immutable.
interface Config {
readonly host: string; // cannot reassign
port?: number; // optional
}
function listen(opts: {host: string; port?: number}) { /* … */ }
listen({host: "0.0.0.0"}); // port omitted
Declarations#
declare introduces an ambient identifier with no
implementation; use it inside .d.ts files to
describe a JavaScript module.
declare const __VERSION__: string; // injected by bundler
declare module "legacy-lib" {
export function compute(x: number): number;
}
type declares a type alias; interface declares an object
or function structure. Both are erased at runtime; see Types
for when to pick which.
type ID = string | number;
interface User { id: ID; name: string; }
Type-only imports#
import type and export type carry only type information;
they are erased at compile time and never trigger a runtime
import.
import type {Config} from "./config.js"; // erased
import {load} from "./loader.js"; // runtime import
For projects with isolatedModules: true (Vite, esbuild, swc),
write import type consistently.
Comments#
JavaScript comments work identically.
// @ts-ignoreand// @ts-expect-errorsuppress a single line of type errors;@ts-expect-erroris preferred because it fails when the error goes away (telling the operator the suppression is now stale)./** … */JSDoc blocks are read by the compiler; useful in plain.jsfiles run through TypeScript withallowJspluscheckJs.