Type annotations

A type annotation states the type of a variable, a parameter or a function's result. It is written after the name, separated by a colon. The checker then makes sure every value stored in that variable, and every use of it, fits the type.

const meterId: string = "E-1001";
const kWh: number = 318.4;
const smartMeter: boolean = true;
const lifetimeWh: bigint = 9_007_199_254_740_993n;

console.log(`${meterId}: ${kWh} kWh this month`);
console.log(`Smart meter: ${smartMeter}`);
console.log(`Lifetime: ${lifetimeWh} Wh`);
E-1001: 318.4 kWh this month
Smart meter: true
Lifetime: 9007199254740993 Wh

Once a variable has a type, it keeps it. Assigning a value of another type is an error, even when JavaScript would accept it without complaint:

let customersAffected: number = 1250;
customersAffected = "1250";
example.ts(2,1): error TS2322: Type 'string' is not assignable to type 'number'.

TS2322, "Type … is not assignable to type …", is the message you will see most often in TypeScript. Read it as "you are putting a string where a number belongs". The annotations disappear when the code is compiled (see What Is TypeScript?); they cost nothing at run time.

The primitive types

Each of JavaScript's primitive types has a TypeScript type of the same name, written in lowercase:

Type Values Example
string Text "INC-30412"
number All numbers, whole or not (a 64-bit floating-point value) 318.4, 1250
boolean true and false true
bigint Whole numbers of any size 10n ** 20n
symbol Unique identifiers Symbol("meter")
null, undefined Only that single value null

number and bigint do not mix, just as in JavaScript, where 1n + 1 throws a TypeError. TypeScript reports it earlier:

const lifetimeWh: bigint = 10n ** 20n;
const kWh: number = lifetimeWh / 1000n;
example.ts(2,7): error TS2322: Type 'bigint' is not assignable to type 'number'.

With strict mode, null and undefined are not part of the other types: a string variable cannot hold null. When a value may be missing you say so with a union such as string | null; both are covered on Union, Intersection and Literal Types and null, undefined and Strict Null Checks.

const label: String = "E-1001";
const id: string = label;
example.ts(2,7): error TS2322: Type 'String' is not assignable to type 'string'.

The full message continues on a second line: "'string' is a primitive, but 'String' is a wrapper object. Prefer using 'string' when possible."

Type inference: when not to annotate

Most of the time you do not have to write the type. When a variable gets a value straight away, TypeScript works the type out from that value; this is type inference. The inferred type is just as fixed as a written one:

let customers = 1250;
customers = "1250";
example.ts(2,1): error TS2322: Type 'string' is not assignable to type 'number'.

Your editor shows the inferred type when you hover over a name. Inference works for arrays, objects and function results too, and it follows values through calls: here the checker knows that id in the callback is a string, so toLowerCase is allowed, and that the result is a string[].

function formatTicket(ticket: { id: string; title: string }): string {
  return `${ticket.id}: ${ticket.title}`;
}
console.log(formatTicket({ id: "INC-30412", title: "Portal login slow" }));

const ticketIds = ["INC-30412", "INC-30415"];
const lower = ticketIds.map((id) => id.toLowerCase());
console.log(lower);
INC-30412: Portal login slow
[ 'inc-30412', 'inc-30415' ]

A practical rule that most TypeScript teams follow:

  • Annotate function parameters (TypeScript cannot guess what callers will pass), the return type of exported functions, and variables that start empty or get their value later.
  • Let inference work for local variables with an initial value and for callback parameters such as id above. Extra annotations there add noise without adding safety.

One detail of inference: let unit = "kWh" gets the type string, because a let may change later, while const tariff = "day" gets the type "day", the one exact value a constant can have. Why that matters is explained in Union, Intersection and Literal Types.

Arrays

An array type describes an array whose elements all have the same type, with any number of them. There are two spellings that mean exactly the same thing: number[] and Array<number>. The first is more common; the second uses the generic form.

const readings: number[] = [11.2, 9.8, 12.5];
const meterIds: Array<string> = ["E-1001", "E-1002"];
readings.push(10.1);
console.log(readings.length, meterIds.join(", "));
readings.push("13.4");
example.ts(5,15): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

Nothing is printed, because the check fails before the code runs: the whole file is checked first, and examples on this site with a type error are never run. (By default tsc would still write the JavaScript; see What Is TypeScript?) An array written with mixed values gets a type that allows each kind: [4.2, "E-1001", null] is inferred as (string | number | null)[]. For a list that must not change, add readonly; the checker then removes every method that would modify it:

const fuseSizesAmps: readonly number[] = [16, 25, 35];
fuseSizesAmps.push(50);
example.ts(2,15): error TS2339: Property 'push' does not exist on type 'readonly number[]'.

Like every type, readonly exists only at compile time. It does not freeze the array at run time; use Object.freeze for that.

Tuples

A tuple type describes an array with a fixed number of elements, each with its own type, in a fixed order: [string, number] is "a string, then a number, and nothing more". At run time it is an ordinary array; the difference is in what the checker allows. Tuples are handy for small groups of values that belong together, such as a key and a value or a function that returns two things. The elements can have labels, which only serve as documentation:

const reading: [string, number] = ["E-1001", 4.2];
const [meterId, kWh] = reading;
console.log(`${meterId} used ${kWh} kWh`);

const peak: [meterId: string, hour: number, kW: number] = ["E-1002", 18, 3.6];
console.log(peak[1], peak.length);
E-1001 used 4.2 kWh
18 3

The checker keeps track of the length and of the type at each position. One element too many is an error, and so is reading a position that does not exist:

const reading: [string, number] = ["E-1001", 4.2, true];
example.ts(1,7): error TS2322: Type '[string, number, boolean]' is not assignable to type '[string, number]'.
const reading: [string, number] = ["E-1001", 4.2];
console.log(reading[2]);
example.ts(2,21): error TS2493: Tuple type '[string, number]' of length '2' has no element at index '2'.

Tuples are the TypeScript counterpart of Python's tuples, with one difference: they are not immutable unless you write readonly [string, number]. For more than two or three elements, an object with named properties is usually clearer (see Objects, Interfaces and Type Aliases).

any: switching the checker off

The any type accepts every value and allows every operation on it: any property, any method call, any assignment. With any the checker simply stops checking. The problem is that it spreads: an any value can be assigned to a typed variable, and from then on the wrong value travels under a correct-looking type.

let raw: any = "18.5";
let kWh: number = raw;
console.log(kWh.toFixed(1));
TypeError: kWh.toFixed is not a function

This compiles without a single error and then crashes at run time, the exact kind of bug TypeScript is meant to prevent. Some built-in functions return any as well; the best known is JSON.parse. A typo on its result is not caught:

const body = JSON.parse('{"meterId": "E-1001", "kWh": 4.2}');
console.log(body.meterId.toLowerCase());
console.log(body.kwh.toFixed(2));
e-1001
TypeError: Cannot read properties of undefined (reading 'toFixed')

any has legitimate uses: while migrating JavaScript step by step, or for a value whose type truly cannot be described. Treat each one as a to-do, not as a fix. Many teams make the linter report every explicit any (see Tooling, Configuration and Migrating from JavaScript).

Implicit any

An implicit any is a parameter or variable that ends up with the type any because it has no annotation and TypeScript cannot infer a type for it. Without strict mode this happens silently; with strict mode (the default since TypeScript 6.0, see Setting Up) the option noImplicitAny turns it into an error:

function formatTicket(ticket) {
  return `${ticket.id}: ${ticket.title}`;
}
console.log(formatTicket({ id: "INC-30412", title: "Portal login slow" }));
example.ts(1,23): error TS7006: Parameter 'ticket' implicitly has an 'any' type.

The fix is the annotation shown in the type inference example. The error is about implicit any only: writing ticket: any is allowed, because then it is a visible decision someone can review.

unknown: the safe alternative

The unknown type also accepts every value, but allows almost nothing with it until your code has checked what it is. It is the honest type for a value from outside the program: parsed JSON, a message from a queue, a caught error.

const body: unknown = JSON.parse('{"meterId": "E-1001", "kWh": 4.2}');
console.log(body.meterId);
example.ts(2,13): error TS18046: 'body' is of type 'unknown'.

You unlock an unknown value with ordinary JavaScript checks such as typeof, Array.isArray and in. Inside each branch the checker knows what the check proved, a process called narrowing:

function describe(value: unknown): string {
  if (typeof value === "string") return `text of length ${value.length}`;
  if (typeof value === "number") return `number ${value.toFixed(1)}`;
  if (Array.isArray(value)) return `list of ${value.length}`;
  return "something else";
}
console.log(describe("INC-30412"));
console.log(describe(4.25));
console.log(describe([1, 2, 3]));
console.log(describe(null));
text of length 9
number 4.3
list of 3
something else

The same works for an object from JSON. Each check in the condition narrows the type further, until the checker knows that body.kWh exists and is a number:

const body: unknown = JSON.parse('{"meterId": "E-1001", "kWh": 4.2}');

if (
  typeof body === "object" && body !== null &&
  "kWh" in body && typeof body.kWh === "number"
) {
  console.log(`Reading: ${body.kWh.toFixed(2)} kWh`);
} else {
  console.log("Not a meter reading");
}
Reading: 4.20 kWh

Writing such checks by hand gets long for bigger objects; reusable type guards and validation libraries are covered in Async Code and Typing API Data.

never: the type with no values

The never type has no values at all. A function whose return type is never never returns normally: it always throws, or runs forever. That is useful for helpers that report an error, because the checker then knows that the code after the call is unreachable:

function fail(message: string): never {
  throw new Error(message);
}

function tariffFor(hour: number): number {
  if (hour >= 7 && hour < 23) return 0.32;
  if (hour >= 0 && hour < 7 || hour === 23) return 0.25;
  return fail(`Invalid hour: ${hour}`);
}

console.log(tariffFor(9), tariffFor(2));
console.log(tariffFor(25));
0.32 0.25
Error: Invalid hour: 25

A function declared as never that could finish normally is an error, so the promise is checked:

function fail(message: string): never {
  console.log(message);
}
example.ts(1,33): error TS2534: A function returning 'never' cannot have a reachable end point.

never also appears by itself when the checker has ruled out every possibility, for example in the last else after all cases of a union have been handled. That is the basis of the exhaustiveness check in Narrowing and Type Guards: if someone adds a new ticket status and forgets to handle it, the code no longer compiles.

Summary: which type when

Situation Type
Local variable with a value Nothing: let inference work
Function parameter Always annotate: (kWh: number)
List of values of one kind number[], or readonly number[] if it must not change
A few values with fixed positions [string, number]
Data from JSON, an API, a file or a user unknown, then check it
A value you really cannot describe any, as a last resort, with a comment why
A function that always throws never
A function that returns nothing void (see Functions)