Technology  /  TypeScript

🔷 TypeScript 16 guides · updated 2026

Type erasure, structural typing, generics, and the type-level programming toolkit — TypeScript taught the way it actually behaves at compile time and at runtime.

Basic Types in TypeScript: Primitives, Arrays, Tuples, and Type Inference

any, unknown, and never all sound like variations on “nothing in particular,” and that vagueness is exactly why developers misuse them interchangeably. They’re not interchangeable — one disables type checking entirely, one forces you to prove a value’s type before using it, and one represents a value that provably cannot exist. Getting this distinction wrong is one of the fastest ways to write TypeScript that compiles cleanly and still crashes in production.

This guide covers the actual type vocabulary you’ll write every day: primitives, arrays, tuples, and the three “nothing” types — with enough of the reasoning behind each to use them correctly instead of by habit.


Primitive Types

let username: string = "alice";
let age: number = 30;
let isActive: boolean = true;
let nothing: null = null;
let notAssigned: undefined = undefined;
let id: symbol = Symbol("id");
let bigNumber: bigint = 9007199254740993n;

TypeScript’s primitives map directly onto JavaScript’s — there’s no separate int/float/double the way Java or C# has. number covers every numeric value, integer or floating-point, which matches how JavaScript itself represents numbers internally (as IEEE 754 doubles). bigint exists specifically for integers beyond Number.MAX_SAFE_INTEGER (2^53 - 1), where ordinary number arithmetic silently loses precision.

Type Inference — You Don’t Always Need to Annotate

let username = "alice"; // inferred as string, no annotation needed
let age = 30; // inferred as number
username = 42; // Error: Type 'number' is not assignable to type 'string'

TypeScript infers a type from the initial value assigned, and that inferred type sticks — reassigning username to a number later is still an error, exactly as if you’d written : string explicitly. This is why experienced TypeScript developers annotate function parameters and return types (where there’s no initial value to infer from) far more often than they annotate simple local variables — the compiler already knows.

function add(a: number, b: number) { // parameters MUST be annotated — no value to infer from
return a + b; // return type inferred as number, no annotation needed
}

Arrays

let scores: number[] = [95, 87, 92];
let names: Array<string> = ["Alice", "Bob"]; // equivalent generic syntax — same meaning as string[]
scores.push(100); // fine
scores.push("100"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'
// Arrays of objects — the shape you'll write constantly in real code
interface Order {
id: string;
amount: number;
}
let orders: Order[] = [
{ id: "A1", amount: 50 },
{ id: "A2", amount: 120 },
];

number[] and Array<number> compile to the identical type — pick whichever reads more naturally for the context; Array<T> tends to read better once T itself gets complex (Array<Map<string, number>> vs the harder-to-parse Map<string, number>[], though both are legal).

Tuples — Fixed-Length Arrays With Known Types Per Position

let point: [number, number] = [10, 20];
let entry: [string, number] = ["temperature", 72];
point[0] = 15; // fine — number at position 0
point[2] = 30; // Error: Tuple type '[number, number]' has no element at index 2
const [label, value] = entry; // label: string, value: number — destructuring keeps per-position types

A plain number[] says “an array of numbers, any length” — it can’t express “exactly two numbers where the first is latitude and the second is longitude.” A tuple [number, number] encodes both the length and the per-position type, which is exactly the shape returned by things like useState() in React ([value, setValue]) or Object.entries() ([key, value] pairs).

// Named tuple members (readability only — doesn't change runtime behavior)
type LatLng = [lat: number, lng: number];
function distanceTo(point: LatLng) { ... }

Named tuple labels are purely documentation for the type — they show up in editor tooltips and error messages but have zero effect on how the tuple is used at runtime; you still destructure or index it positionally.

// Optional and rest elements in tuples
type RGB = [number, number, number];
type RGBA = [number, number, number, alpha?: number]; // 4th element optional
type StringsThenNumbers = [string, ...number[]]; // fixed string, then any number of numbers

any — Turning Off Type Checking

let data: any = fetchFromUnknownSource();
data.whatever.deeplyNested.property; // no error — any operation on `any` is allowed
data(); // no error — even calling it as a function is allowed
data.toUpperCase().length.foo.bar; // still no error, all the way down

any is the type equivalent of turning TypeScript off for that specific value — every property access, method call, and assignment involving an any-typed value skips checking entirely, and that “skip checking” property is contagious: anything derived from an any value is also implicitly any unless you explicitly re-annotate it. It exists as an escape hatch for genuinely dynamic situations (deserializing arbitrary JSON, interfacing with untyped legacy code) — but every any in a codebase is a spot where the type system has given up, silently, and the compiler will not warn you about mistakes made through it.

unknown — The Type-Safe Alternative to any

let data: unknown = fetchFromUnknownSource();
data.toUpperCase(); // Error: Object is of type 'unknown'
console.log(data); // fine — this doesn't require knowing the type
if (typeof data === "string") {
data.toUpperCase(); // fine now — TypeScript narrowed data to string inside this block
}

unknown represents the same real-world situation as any — “I genuinely don’t know this value’s type yet” — but it forces you to prove the type before doing anything type-specific with it, through a narrowing check like typeof, instanceof, or a custom type guard. This is the correct default for anything coming from outside your program’s control — API responses, JSON.parse() results, user input — precisely because it can’t be misused by accident the way any can. If a function signature ever tempts you to write any for “I don’t know what this is yet,” unknown is very likely what you actually mean.

disables checking entirely,

contagious to everything derived from it

requires narrowing before use

any

Use only when you

deliberately want no safety

unknown

Use for genuinely unknown

external data — the safe default

never — The Type of Values That Cannot Exist

function throwError(message: string): never {
throw new Error(message); // this function never successfully returns a value
}
function infiniteLoop(): never {
while (true) {}
}

never is the return type of a function that never completes normally — it either always throws or never returns at all (an infinite loop). This is distinct from void, which means “returns, but with no meaningful value” — a void function still returns control to its caller; a never function does not.

// never also appears from exhaustive narrowing — a genuinely useful compile-time check
type Shape = { kind: "circle"; radius: number } | { kind: "square"; side: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "square": return shape.side ** 2;
default:
const exhaustiveCheck: never = shape; // Error here if a new Shape variant is added and unhandled
throw new Error(`Unhandled shape: ${exhaustiveCheck}`);
}
}

This “exhaustiveness check” pattern is one of never’s most practical real-world uses: if someone later adds a { kind: "triangle"; ... } variant to the Shape union but forgets to handle it in area(), the default branch’s shape is no longer narrowed all the way down to never (there’s an unhandled case left), and the assignment to exhaustiveCheck: never fails to compile — catching the missed case at compile time instead of at runtime when a triangle silently returns undefined from area().

void — “Returns Nothing Meaningful”

function logMessage(message: string): void {
console.log(message);
// no return statement, or `return;` with no value — both fine for void
}
const callback: () => void = () => {
console.log("done");
};

void is overwhelmingly used for function return types, signaling “call this for its side effect, not for its return value.” Unlike never, a void function completes normally and returns control — it simply doesn’t return anything the caller is expected to use.


Type Aliases for Basic Shapes

type ID = string | number;
type Coordinates = [number, number];
type Callback = (error: Error | null, result?: string) => void;
function findRecord(id: ID) { ... }

A type alias doesn’t create a new type at runtime (remember: type erasure) — it just gives an existing type shape a reusable name, which matters once a type gets used in more than one place and you don’t want to repeat string | number everywhere.

Literal Types — Values as Types

let direction: "north" | "south" | "east" | "west";
direction = "north"; // fine
direction = "up"; // Error: Type '"up"' is not assignable to type '"north" | "south" | "east" | "west"'
let statusCode: 200 | 404 | 500;

A literal type narrows a primitive down to one specific value (or, combined with |, a specific set of allowed values) — this is what powers autocomplete suggestions in your editor for string arguments, and it’s a lightweight, boilerplate-free alternative to defining an enum for a small fixed set of string options.

// const assertions lock a value's type down to its literal form
let a = "hello"; // type: string
let b = "hello" as const; // type: "hello" — the literal, not the general string
const config = { mode: "production" } as const;
// config.mode: "production", not string — useful when passing config into APIs expecting a literal union

Type Checking Quick Reference

TypeMeaningTypical use
string, number, booleanStandard primitivesEveryday values
T[] / Array<T>Variable-length list, single element typeCollections
[T, U]Fixed-length, per-position typesCoordinate pairs, useState-style returns
anyAll checking disabledAvoid — use only as a deliberate, documented escape hatch
unknownUnknown, but must be narrowed before useExternal/untrusted data
neverCannot occur — always throws, loops forever, or is exhaustively eliminatedError-throwing helpers, exhaustiveness checks
voidReturns, but with nothing meaningfulSide-effecting function return types
Literal types ("a" | "b")One of a fixed, known set of valuesSmall enumerations without a formal enum

Frequently Asked Questions

Should I ever use any? Rarely, and deliberately — for genuinely dynamic interop (some untyped third-party library, a deliberately loose escape hatch during a migration) where you’re explicitly choosing to opt out of checking for that one value. Reach for unknown first every time you’re tempted by any for “I don’t know the type yet.”

What’s the difference between undefined and void? undefined is a value (and a type containing just that value) that a variable can genuinely hold. void is specifically a function return-type annotation meaning “don’t use this function’s return value” — you’d never write let x: void.

Why does TypeScript infer number for a variable but require explicit typing for function parameters? Inference needs something to infer from — a variable’s initializer gives the compiler a concrete value to look at. A function parameter has no value until the function is called, so there’s nothing to infer from at the point the function is defined; TypeScript would otherwise have to fall back to any, which noImplicitAny (see the project setup guide) correctly flags as an error.

Is [number, number] really different from number[] at runtime? No — both compile to a plain JavaScript array, and type erasure means the distinction is purely compile-time. The value in the tuple type is entirely in what the compiler will let you do with it before that erasure happens.

When should I reach for a literal union instead of an enum? For small, string-based fixed sets that don’t need reverse mapping or a namespace-like grouping, a literal union ("pending" | "shipped" | "delivered") is simpler, has zero runtime footprint, and integrates more naturally with plain JavaScript objects than an enum does. The enums guide covers exactly when the trade-off flips the other way.


What’s Next

You now have the vocabulary for describing shapes of data — primitives, arrays, tuples, and the crucial distinctions between any, unknown, never, and void. Next: Functions in TypeScript, where these types get applied to parameters, return values, overloads, and the trickiest part of typing functions correctly — this.