Basic Types and Type Annotations
Annotating variables with string, number, boolean, arrays and tuples, letting the compiler infer types, and the special types any, unknown and never.
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
idabove. 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)
|