On this page
Primitive types and annotations
The seven primitive types
TypeScript inherits JavaScript's seven primitive types and adds precise static typing on top of each one. A primitive is an immutable value that is not an object — it is stored directly, not by reference.
string
Represents text. TypeScript accepts single quotes, double quotes, and template literals — all have the same type.
const firstName: string = "Alice";
const lastName: string = 'Wonderland';
const fullName: string = `${firstName} ${lastName}`;
// Multiline template literal — still a string
const message: string = `
Hello, ${firstName}!
Welcome back.
`;number
TypeScript has a single number type for both integers and floating-point values, matching JavaScript's IEEE 754 double-precision format.
const price: number = 9.99;
const quantity: number = 3;
const hex: number = 0xff; // 255
const binary: number = 0b1010; // 10
const octal: number = 0o17; // 15
const large: number = 1_000_000; // numeric separators for readabilityBe aware that number cannot represent integers larger than Number.MAX_SAFE_INTEGER (2^53 - 1) without precision loss.
boolean
Only two values: true and false.
const isLoggedIn: boolean = false;
const hasPermission: boolean = true;
// Computed boolean — inference works perfectly here
const canEdit = isLoggedIn && hasPermission; // inferred as booleannull and undefined
These are separate types in TypeScript with strict mode enabled. null means "intentionally absent value". undefined means "this variable has not been assigned yet".
// With strictNullChecks enabled (always on with strict: true):
let value: string = "hello";
value = null; // Error: Type 'null' is not assignable to type 'string'
value = undefined; // Error: Type 'undefined' is not assignable to type 'string'
// Use a union to allow null or undefined explicitly
let nullable: string | null = "hello";
nullable = null; // OK
let optional: string | undefined = "world";
optional = undefined; // OKbigint
The bigint type represents arbitrarily large integers, solving the precision problem of number for very large values. Bigint literals end with n.
const maxSafeInt: number = Number.MAX_SAFE_INTEGER; // 9007199254740991
const beyondMax: bigint = 9_007_199_254_740_993n; // exact
// Arithmetic works naturally with bigint
const a: bigint = 100n;
const b: bigint = 200n;
const sum: bigint = a + b; // 300n
// You cannot mix bigint and number without explicit conversion
const mixed = a + 1; // Error: Cannot mix BigInt and other types
const converted = a + BigInt(1); // OK: 101nsymbol
Symbol() creates a unique, non-comparable identifier. No two symbols are ever equal, even if they have the same description.
const id1: symbol = Symbol("id");
const id2: symbol = Symbol("id");
console.log(id1 === id2); // false — always unique
// Symbols are often used as object keys to avoid collisions
const SERIALIZABLE = Symbol("serializable");
class DataModel {
[SERIALIZABLE] = true;
}Type annotations vs type inference
TypeScript can infer types automatically from values. You do not need to annotate every variable — in fact, redundant annotations are considered noise in modern TypeScript style guides.
// Redundant annotations — avoid these
const name: string = "Alice";
const count: number = 0;
const active: boolean = true;
// Preferred — let TypeScript infer from the value
const name = "Alice"; // inferred: string
const count = 0; // inferred: number
const active = true; // inferred: booleanThe rule of thumb: annotate at boundaries, infer in the middle.
Annotate:
- Function parameters — TypeScript cannot infer these
- Function return types — makes your API explicit and catches bugs early
- Variables initialized to
nullorundefined - Variables declared without an initial value
Let TypeScript infer:
- Local variables with obvious types
- Intermediate computed values inside functions
- Variables assigned from typed function return values
// Annotate: parameters and return type
function formatCurrency(amount: number, currency: string): string {
return `${currency}${amount.toFixed(2)}`;
}
// Infer: the return value is clearly a string
const label = formatCurrency(9.99, "$"); // inferred: stringThe any trap
any is an escape hatch that completely disables TypeScript's type checking for a value. It is sometimes necessary when working with untyped third-party code or during a migration, but it should be treated as a last resort, not a convenience.
let data: any = fetchFromExternalApi();
// TypeScript accepts every single one of these — no errors reported:
data.nonExistentMethod(); // will crash at runtime
data[0].anything; // will crash at runtime
const result = data * "foo"; // produces NaN silentlyThe danger is not just that any bypasses checks on that variable — any also spreads to everything it touches. A function that accepts or returns any poisons the types of all its callers.
unknown: the safe alternative
unknown is the type-safe counterpart to any. A value of type unknown can hold anything, but TypeScript forces you to narrow its type before you use it.
function processInput(input: unknown): string {
// TypeScript rejects this: Object is of type 'unknown'
// return input.trim();
// You must narrow first
if (typeof input === "string") {
return input.trim(); // here TypeScript knows input is string
}
if (typeof input === "number") {
return input.toFixed(2);
}
return String(input);
}Use unknown for:
- JSON-parsed data where the structure is not guaranteed
- Values from external APIs without TypeScript types
- Generic utility functions that operate on arbitrary input
- Error objects in catch blocks (
useUnknownInCatchVariablesis enabled bystrict)
void and never
void is the return type for functions that do not return a value. It signals to callers that the return value should be ignored.
function logMessage(message: string): void {
console.log(`[LOG] ${message}`);
// No return statement needed — void functions implicitly return undefined
}
// void is also used in callback types
type EventHandler = (event: MouseEvent) => void;never represents values that never occur. A function returning never either throws unconditionally, or loops forever. TypeScript also infers never in exhaustive checks.
// Function that always throws — return type is never
function fail(message: string): never {
throw new Error(message);
}
// Function that never returns — return type is never
function infiniteLoop(): never {
while (true) {
processQueue();
}
}
// never in exhaustive type checks
type Direction = "north" | "south" | "east" | "west";
function handleDirection(direction: Direction): string {
switch (direction) {
case "north": return "heading north";
case "south": return "heading south";
case "east": return "heading east";
case "west": return "heading west";
default: {
// If you add a new Direction and forget to handle it here,
// TypeScript reports an error on the next line
const exhaustiveCheck: never = direction;
return exhaustiveCheck;
}
}
}The never check in the default branch is a powerful pattern: if you ever add a new direction without updating this switch, TypeScript will report a compile error immediately.
Practice
- Write a function
safeParseJSON(raw: unknown): Record<string, unknown>that parses a JSON string. Useunknownfor the parsed value, narrow it with type guards, and return a typed result. - Create a variable declared as
string | nulland write a helper function that accepts it, handles the null case, and returns a non-null string. Observe how TypeScript narrows the type inside each branch. - Implement an exhaustive switch for a union type with three members. Then add a fourth member to the union and confirm TypeScript reports an error at the
default: neverbranch.
// The seven primitive types in TypeScript
const name: string = "Alice";
const age: number = 30;
const isActive: boolean = true;
const nothing: null = null;
const notSet: undefined = undefined;
const bigNumber: bigint = 9_007_199_254_740_993n;
const id: symbol = Symbol("userId");
// Type inference — no annotation needed when the value is obvious
const greeting = "Hello"; // inferred as string
const count = 42; // inferred as number
const enabled = false; // inferred as boolean
Sign in to track your progress