Basic Types
The vocabulary of the type system: what you can write on the right-hand side of a colon, and what TypeScript writes there for you when you don’t.
Primitives
Section titled “Primitives”The lowercase names are the ones you want. There is one per JavaScript primitive:
let name: string = 'Ada';let age: number = 36; // all numbers: integers, floats, Infinity, NaNlet big: bigint = 9007199254740993n;let active: boolean = true;let nothing: null = null;let missing: undefined = undefined;let key: symbol = Symbol('id');Most of these annotations are redundant — see inference below. Write
const name = 'Ada' and TypeScript already knows.
null and undefined under strictNullChecks
Section titled “null and undefined under strictNullChecks”With strictNullChecks on (part of strict, and you should have it on), null and undefined
are not members of every type. This is the single most valuable check TypeScript performs.
let s: string = null; // error: Type 'null' is not assignable to type 'string'let t: string | null = null; // fine — you said so explicitly
function len(x: string | null): number { return x.length; // error: 'x' is possibly 'null'}You fix that by narrowing:
function len(x: string | null): number { if (x === null) return 0; return x.length; // x is `string` here}Arrays
Section titled “Arrays”Two equivalent syntaxes:
const scores: number[] = [1, 2, 3];const names: Array<string> = ['a', 'b'];T[] is conventional; Array<T> is occasionally clearer for complex element types, e.g.
Array<{ id: number; tags: string[] }> versus { id: number; tags: string[] }[].
Arrays of unions need parentheses — the two spellings mean different things:
let a: (string | number)[]; // array whose elements are string or numberlet b: string | number[]; // a string, OR an array of numbersRead-only arrays reject mutation at compile time:
const frozen: readonly number[] = [1, 2, 3];frozen.push(4); // error: Property 'push' does not exist on type 'readonly number[]'const doubled = frozen.map((n) => n * 2); // fine — map doesn't mutatereadonly T[] and ReadonlyArray<T> are the same type. Note readonly is shallow: the elements
themselves are still mutable if they’re objects.
Tuples
Section titled “Tuples”A tuple is an array with a fixed length and a type per position.
type Point = [number, number];const origin: Point = [0, 0];
const [x, y] = origin; // both numberLabels are allowed and show up in editor tooltips — purely cosmetic, but worth adding:
type Rgb = [red: number, green: number, blue: number];Optional and rest elements:
type Range = [start: number, end: number, step?: number];type NonEmpty = [string, ...string[]]; // at least one elementtype Args = [name: string, ...flags: boolean[]];Tuples are what makes React’s useState work:
function useToggle(initial: boolean): [boolean, () => void] { // ... return [initial, () => {}];}const [isOpen, toggle] = useToggle(false); // isOpen: boolean, toggle: () => voidenum is one of the few TypeScript features that emits runtime code.
enum Direction { Up, // 0 Down, // 1 Left, // 2 Right, // 3}
const d: Direction = Direction.Up;That compiles to a real object, and numeric enums get a reverse mapping:
var Direction;(function (Direction) { Direction[Direction["Up"] = 0] = "Up"; Direction[Direction["Down"] = 1] = "Down"; // ...})(Direction || (Direction = {}));So Direction[0] === 'Up' at runtime. String enums do not get reverse mappings:
enum Status { Active = 'ACTIVE', Banned = 'BANNED',}Status.Active; // => 'ACTIVE'const enum
Section titled “const enum”const enum has no runtime representation; every usage is inlined.
const enum Level { Low = 1, High = 2 }const l = Level.High; // emits: const l = 2 /* Level.High */When a union is better
Section titled “When a union is better”For the common case of “one of these string values”, a union of string literals is usually the better tool:
type Status = 'active' | 'banned' | 'pending';
function setStatus(s: Status) {}setStatus('active'); // finesetStatus('deleted'); // error, with a helpful suggestionCompared to enums, unions:
- Emit nothing — pure type-level, fully erasable.
- Accept plain string literals, so they interoperate with JSON, APIs, and untyped callers.
- Work with template literal types and other type-level machinery.
Enums win when you need a runtime object to iterate over or map through. If you want both, use an
object with as const:
const Status = { Active: 'active', Banned: 'banned',} as const;
type Status = (typeof Status)[keyof typeof Status]; // 'active' | 'banned'
Object.values(Status); // runtime iteration worksany, unknown, never, void
Section titled “any, unknown, never, void”These four confuse people because they sound similar and behave nothing alike.
| Type | Meaning | Can you assign to it? | Can you use the value? |
|---|---|---|---|
any |
Checking disabled | Anything | Anything — no checks at all |
unknown |
Some value, identity unproven | Anything | Nothing until you narrow |
never |
No value is possible | Nothing | N/A |
void |
Return value should be ignored | undefined (and null without strict) |
No |
any — the escape hatch
Section titled “any — the escape hatch”any turns the checker off for that value. It is assignable to and from everything, and it
propagates silently.
const data: any = JSON.parse('{"a":1}');data.b.c.d; // no errordata(); // no errorconst n: number = data; // no error — the infection spreadsany is sometimes necessary, but every any is a hole in the type system. Enable noImplicitAny
(part of strict) so the compiler never gives you one by accident.
unknown — the safe any
Section titled “unknown — the safe any”unknown accepts any value but lets you do nothing with it until you prove what it is.
const data: unknown = JSON.parse('{"a":1}');
data.a; // error: 'data' is of type 'unknown'const n: number = data; // error
if (typeof data === 'object' && data !== null && 'a' in data) { console.log(data.a); // now allowed}This is the correct type for anything crossing a trust boundary: JSON.parse results, catch
variables, values from postMessage, third-party callbacks.
never — the impossible type
Section titled “never — the impossible type”never is the type with no values. It arises in three places:
// 1. A function that never returns normally.function fail(msg: string): never { throw new Error(msg);}
// 2. An impossible narrowing.function f(x: string) { if (typeof x === 'number') { x; // never — a string can't be a number }}
// 3. Filtering a union down to nothing.type Empty = Exclude<'a' | 'b', 'a' | 'b'>; // neverIts practical use is exhaustiveness checking — see
unions and narrowing. never is assignable to every type
(the empty set is a subset of everything), and nothing except never is assignable to it.
void — “ignore my return value”
Section titled “void — “ignore my return value””void is the return type of a function that returns nothing useful. It is not the same as
undefined:
function log(msg: string): void { console.log(msg);}The important quirk: a function returning something is assignable to a position expecting a
void-returning function. This is deliberate and makes callbacks ergonomic.
type Handler = () => void;
const h: Handler = () => 42; // allowedconst r = h(); // typed void — the value is there at runtime but you can't use it
// The reason this matters:const nums: number[] = [];[1, 2, 3].forEach((n) => nums.push(n)); // push returns number; forEach wants void. Fine.Literal types and literal narrowing
Section titled “Literal types and literal narrowing”A literal type is a type containing exactly one value.
let a: 'hello' = 'hello';a = 'world'; // error: Type '"world"' is not assignable to type '"hello"'You rarely write those directly. What matters is that const produces them automatically:
const greeting = 'hello'; // type: 'hello' (literal — it can never change)let mutable = 'hello'; // type: string (widened — it might change)This is literal widening. let and mutable object properties widen literals to their base
type, because the value could be reassigned. const doesn’t, because it can’t.
The consequence bites when you build an object and pass it somewhere:
type Method = 'GET' | 'POST';function request(url: string, method: Method) {}
const opts = { url: '/api', method: 'GET' }; // method inferred as stringrequest(opts.url, opts.method);// error: Argument of type 'string' is not assignable to parameter of type 'Method'Three fixes, in rough order of preference:
// 1. Annotate the object.const opts: { url: string; method: Method } = { url: '/api', method: 'GET' };
// 2. Freeze the literals with `as const`.const opts = { url: '/api', method: 'GET' } as const; // method: 'GET'
// 3. Assert just the one property.const opts = { url: '/api', method: 'GET' as Method };as const deserves emphasis. It marks an expression as deeply immutable and stops all widening:
const config = { retries: 3, mode: 'strict', tags: ['a', 'b'],} as const;// {// readonly retries: 3;// readonly mode: 'strict';// readonly tags: readonly ['a', 'b'];// }Type inference
Section titled “Type inference”TypeScript infers types from initialisers, return expressions, default values, and the expected type of the surrounding context. Most code needs no annotations.
const n = 42; // 42 (const) — widens to number when used as a valuelet m = 42; // numberconst list = [1, 2, 3]; // number[]const mixed = [1, 'a']; // (string | number)[]const user = { id: 1, name: 'x' }; // { id: number; name: string }
function double(n: number) { // return type inferred: number return n * 2;}Contextual typing works in the other direction — the expected type flows into an expression:
const nums = [1, 2, 3];nums.forEach((n) => n.toFixed(2)); // `n` is number; no annotation needed
window.addEventListener('click', (e) => { e.clientX; // `e` is MouseEvent, inferred from the event name});When to annotate
Section titled “When to annotate”- Always annotate function parameters. There is nothing to infer them from. Without
noImplicitAny, they’d silently beany. - Annotate exported/public function return types if you want a stable contract and clearer errors. Inside a module, inferred returns are fine and reduce churn.
- Annotate empty containers.
const items = []isany[](or evolvingany[]under strict); writeconst items: string[] = []. - Annotate when you want the checker to verify a value, not describe it:
// The annotation checks the literal against Config.const config: Config = { /* missing fields are errors here */ };type vs interface, briefly
Section titled “type vs interface, briefly”Both name a type. For plain object shapes they are nearly interchangeable:
interface User { id: number; name: string;}
type User2 = { id: number; name: string;};The differences, in one line each:
interfacecan be reopened — declare it twice and the members merge.typecannot.typecan name anything: unions, tuples, primitives, mapped and conditional types.interfaceonly describes object shapes (including callable and constructable ones).interfaceusesextends;typeuses&(intersection).
Full treatment in functions and objects.
Key points
Section titled “Key points”- Use lowercase primitive names; never
String/Number/Boolean. strictNullCheckskeepsnullandundefinedout of other types — keep it on.T[]for arrays, fixed-length[A, B]for tuples; array literals needas constor an annotation to become tuples.- Prefer string-literal unions over
enum; use anas constobject when you also need runtime values. anydisables checking,unknowndemands proof,nevermeans impossible,voidmeans “ignore the return value”.constproduces literal types;letwidens.as constfreezes an entire expression.- Annotate boundaries; let inference handle the rest.