Each type describes a set of the JavaScript values from JavaScript: number is every number, "GET" is one string, string | null is every string plus null. A small vocabulary covers most code.
| Type | Example annotation | Meaning |
|---|---|---|
| string, number, boolean, bigint, symbol | let price: number = 9.5 | Lowercase; no separate int or float |
| null, undefined | let el: Element | null | Separate types under strictNullChecks |
| Array | string[] or Array<string> | [string, number] is a tuple (Object Types) |
| Object type | { x: number; y?: number } | ? marks an optional property |
| Union | number | string | A value of either type |
| Literal | "left" | "right" | Exactly these values |
| Function | (n: number) => string | Parameters and result (More on Functions) |
| void | function log(): void | Returns no useful value |
| any | let raw: any | Opts out of checking |
| unknown | let input: unknown | Anything; must be narrowed before use |
| never | function fail(): never | No value: throws or cannot happen |
Annotations and inference
An annotation follows the name after a colon. Variables rarely need one: TypeScript infers the type from the initializer and keeps it, so a let that starts as a number stays a number. Annotate function parameters, and the return type of exported functions to document intent. Callbacks need nothing: contextual typing gives tag in tags.forEach((tag) => ...) the array's element type.
let title: string = "TypeScript"; // annotation (redundant here)
let count = 42; // inferred: number
const tags = ["ts", "js"]; // inferred: string[]
function average(values: number[]): number {
return values.reduce((sum, v) => sum + v, 0) / values.length;
}
tags.forEach((tag) => console.log(tag.toUpperCase())); // tag: string
count = "many";
average(tags);
title.toUppercase();everyday.ts(10,1): error TS2322: Type 'string' is not assignable to type 'number'. everyday.ts(11,9): error TS2345: Argument of type 'string[]' is not assignable to parameter of type 'number[]'. Type 'string' is not assignable to type 'number'. everyday.ts(12,7): error TS2551: Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?
Object types, unions, aliases and interfaces
An object type lists property names and their types; ? makes a property optional (its type gains undefined) and readonly forbids reassignment. A union A | B accepts either member, but you may only use operations valid for every member, so id.toFixed() fails when id might be a string. Give any type a reusable name with a type alias, or name an object shape with an interface.
type Id = number | string; // type alias for a union
interface User {
id: Id;
name: string;
email?: string; // optional: string | undefined
readonly createdAt: Date;
}
interface User { roles: string[] } // interfaces merge; aliases cannot
function label(user: User): string {
const mail = user.email.toLowerCase();
return `${user.name} #${user.id.toFixed(0)}`;
}
const ada: User = { id: 1, name: "Ada", createdAt: new Date(), roles: [], age: 36 };
ada.createdAt = new Date();
type Id = bigint;objects.ts(1,6): error TS2300: Duplicate identifier 'Id'. objects.ts(12,16): error TS18048: 'user.email' is possibly 'undefined'. objects.ts(13,35): error TS2339: Property 'toFixed' does not exist on type 'Id'. Property 'toFixed' does not exist on type 'string'. objects.ts(16,75): error TS2353: Object literal may only specify known properties, and 'age' does not exist in type 'User'. objects.ts(17,5): error TS2540: Cannot assign to 'createdAt' because it is a read-only property. objects.ts(18,6): error TS2300: Duplicate identifier 'Id'.
TypeScript is structurally typed: any object with the right properties is a User. The age error is an excess property check, applied only to object literals written in place, where an extra property is usually a typo. Narrowing (Narrowing) fixes the email and id errors.
| interface | type alias | |
|---|---|---|
| Names | Object shapes only | Any type: unions, tuples, primitives |
| Extending | interface B extends A {} | Intersection type B = A & {} |
| Re-opening | Declarations merge | Duplicate identifier error |
The handbook's rule of thumb: use interface until you need a feature only type offers. Merging is how libraries let you add properties to globals such as Window (Reference Topics).
Literal types and as const
A const holding "GET" has the literal type "GET", but a let or an object property is widened to string because it could change later. To keep an object's literal types, add as const, which also makes it readonly.
type Method = "GET" | "POST";
declare function send(url: string, method: Method): void;
let loose = "GET"; // widened to string
const req = { url: "/api", method: "GET" };
const frozen = { url: "/api", method: "GET" } as const;
send(req.url, loose);
send(req.url, req.method);
send(frozen.url, frozen.method); // OK: method is "GET"
const canvas = document.getElementById("chart") as HTMLCanvasElement;
const input = document.querySelector("input")!; // trust me: not null
const n = "42" as number;
const data: unknown = JSON.parse('{"x":1}');
data.x;
const risky: any = JSON.parse('{"x":1}');
risky.y.z(); // no error, crashes at runtimeliterals.ts(8,15): error TS2345: Argument of type 'string' is not assignable to parameter of type 'Method'. literals.ts(9,15): error TS2345: Argument of type 'string' is not assignable to parameter of type 'Method'. literals.ts(14,11): error TS2352: Conversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. literals.ts(17,1): error TS18046: 'data' is of type 'unknown'.
Assertions, any and unknown
A type assertion expr as T tells the checker you know better. getElementById returns HTMLElement | null, and only you know that #chart is a canvas. Assertions are erased, so nothing checks them at runtime, and TypeScript allows them only toward a more or less specific type: "42" as number is rejected. The postfix ! is a shorthand assertion that removes null and undefined. Both are promises you must keep.
any switches checking off in both directions and spreads silently: JSON.parse returns any, so risky.y.z() compiles and throws a TypeError when it runs. Annotate such values as unknown instead; the checker then refuses every operation until a typeof, in or custom guard proves what the value is.