Polyglot
Languages TypeScript types
TypeScript § types

Types

The TypeScript type system is static, structural, and gradual. The principal types are the JavaScript primitives (string, number, boolean, null, undefined, bigint, symbol), object types (interfaces, type aliases, classes), function types, array and tuple types, the top types (any, unknown), the bottom type (never), and the empty type (void). Literal types admit specific values as types ("hello", 42, true); union types (A | B) admit either of several types; intersection types (A & B) require both. The combination — structural compatibility, literal types, unions and intersections, generics — covers a substantial expressive surface; the conventional contemporary discipline uses strict: true to enforce non-nullability and additional checks.

Primitive types

The JavaScript primitives:

let n: number = 42;                              // 64-bit IEEE 754 float
let s: string = "hello";
let b: boolean = true;
let big: bigint = 100n;                          // arbitrary-precision integer
let sym: symbol = Symbol("name");
let nothing: undefined = undefined;
let nada: null = null;

The number type is the standard JavaScript number — IEEE 754 double-precision; integer arithmetic is exact only up to 2^53.

The bigint admits arbitrary-precision integers, with the suffix n on literals (100n).

null and undefined are distinct values:

  • undefined — the conventional “no value yet”; default for unassigned variables and missing properties.
  • null — the conventional “intentionally absent”; less commonly used in idiomatic TypeScript.

Under strict: true, null and undefined are not assignable to other types:

let n: number = null;                            // ERROR under strict
let n: number | null = null;                     // OK; explicit union

Literal types

A literal type admits a single specific value:

let x: 42 = 42;                                  // x must be 42
let x: 42 = 43;                                  // ERROR

let s: "hello" = "hello";                        // s must be "hello"
let active: true = true;                         // active must be true

The conventional uses are enum-like unions:

type Status = "active" | "inactive" | "banned";
type Direction = "north" | "south" | "east" | "west";
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";

For mixed types, unions admit substantial flexibility:

type Result = "ok" | { error: string };
type Maybe = number | null;

Union types

The | admits “either”:

let id: string | number;
id = "abc";                                      // OK
id = 42;                                         // OK
id = true;                                       // ERROR

function format(v: string | number): string {
    return String(v);
}

Working with a union value requires narrowing — the compiler must verify that the value is the right type before type-specific operations:

function format(v: string | number): string {
    if (typeof v === "string") {
        return v.toUpperCase();                  // OK; v is narrowed to string
    }
    return v.toFixed(2);                          // OK; v is narrowed to number
}

Treated in Narrowing.

Intersection types

The & admits “both”:

type Named = { name: string };
type Aged = { age: number };

type Person = Named & Aged;
const p: Person = { name: "Alice", age: 30 };

The intersection requires the value to satisfy all the components. The mechanism is conventional for combining mixin-style types and adding properties to existing types.

Type aliases vs interfaces

Two principal forms for declaring object types:

// Type alias:
type Point = {
    x: number;
    y: number;
};

// Interface:
interface Point {
    x: number;
    y: number;
}

Both admit:

  • Property and method declarations.
  • Optional properties (name?: string).
  • Readonly properties (readonly id: number).
  • Generic type parameters.
  • Extension (interfaces use extends; type aliases use &).

The principal differences:

Featuretypeinterface
Object typesYesYes
Union and intersectionYesOnly via extends
Tuple, primitive aliasesYesNo
Conditional and mapped typesYesNo
Declaration mergingNoYes
extends syntaxVia &Via extends

The conventional discipline:

  • Use interface for object types meant to be implemented or extended.
  • Use type for unions, tuples, mapped types, and other non-object types.
  • Use either consistently within a project.

Object types

Object types admit specifying the shape of objects:

let user: {
    name: string;
    age: number;
    email?: string;                              // optional
    readonly id: number;                          // immutable
};

user = { name: "Alice", age: 30, id: 1 };
user.id = 2;                                     // ERROR: readonly
user.email = "alice@example.com";                // OK

For inline shapes, the literal form is admitted; for reusable shapes, an interface or type alias is conventional.

Index signatures

Object types admit index signatures — describing arbitrary keys:

interface Dictionary {
    [key: string]: string;
}

const d: Dictionary = {
    foo: "bar",
    baz: "qux",
};

interface NumericDict {
    [key: number]: string;
}

For typed records with known keys, Record<K, V> is conventionally clearer:

type Settings = Record<string, string>;
type StatusCounts = Record<"active" | "inactive", number>;

Array types

Two forms:

let a: number[] = [1, 2, 3];                     // shorthand
let a: Array<number> = [1, 2, 3];                // generic form

let mixed: (string | number)[] = ["a", 1, "b", 2];

The two forms are equivalent; the shorthand is conventional. The Array<T> form is conventional when T is itself a complex type.

For readonly arrays:

const a: readonly number[] = [1, 2, 3];
const a: ReadonlyArray<number> = [1, 2, 3];

a.push(4);                                       // ERROR: not on readonly
a[0] = 10;                                       // ERROR: readonly

Tuple types

Tuples admit fixed-length, heterogeneously-typed arrays:

let pair: [string, number] = ["alice", 30];
let triple: [number, number, string] = [1, 2, "three"];

const [name, age] = pair;                        // destructuring works

// Optional elements:
let opt: [string, number?] = ["alice"];

// Rest elements:
let stringsThenNumber: [...string[], number] = ["a", "b", "c", 42];

// Readonly tuples:
const point: readonly [number, number] = [1, 2];

Tuples are conventional for “named return values” patterns and React’s useState-style hooks:

function pair(): [string, number] {
    return ["alice", 30];
}

const [name, age] = pair();

Function types

Two forms:

// Function type expression:
type Predicate<T> = (value: T) => boolean;

const isEven: Predicate<number> = (n) => n % 2 === 0;

// Call signature in object type:
interface Comparator<T> {
    (a: T, b: T): number;
}

const compareNumbers: Comparator<number> = (a, b) => a - b;

Treated in Functions.

any

The any type is the escape hatch — values typed as any admit all operations:

let x: any = 42;
x = "hello";                                     // OK
x.foo.bar.baz();                                 // OK; not type-checked

const arr: any[] = [1, "two", true];

The conventional discipline avoids any — it disables type checking. The conventional substitutes:

  • unknown — for genuinely unknown values; type-narrowing required before use.
  • Generics — for code that works with arbitrary types.
  • Specific types — for known shapes.

Where any appears in practice, it conventionally indicates incomplete typing of legacy code; // eslint-disable-next-line @typescript-eslint/no-explicit-any is the conventional suppression.

unknown

The unknown type is the type-safe any — values typed unknown admit no operations until narrowed:

let x: unknown = 42;

x.toString();                                    // ERROR: x might not be a string
x = "hello";

if (typeof x === "string") {
    x.toUpperCase();                              // OK; x is narrowed to string
}

The conventional uses are JSON parsing, catch clause variables (since TS 4.4 with useUnknownInCatchVariables), and any boundary where the type is genuinely unknown.

void

The void type indicates “no return value”:

function log(message: string): void {
    console.log(message);
}

const f: () => void = () => {};

A void return type does not strictly mean “returns undefined”; it admits returning any value (which is then ignored at the call site).

never

The never type represents “no value can exist” — for functions that never return (throw or infinite loop) and unreachable code:

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

function infiniteLoop(): never {
    while (true) {}
}

// Exhaustiveness check:
function classify(x: "a" | "b"): string {
    switch (x) {
        case "a": return "first";
        case "b": return "second";
        default:
            const _exhaustive: never = x;        // ERROR if a case is missing
            return _exhaustive;
    }
}

The exhaustiveness check is one of never’s principal uses; treated in Pattern dispatch.

Type assertions

A type assertion tells the compiler “trust me, this value is of this type”:

const len = (someValue as string).length;

const input = document.getElementById("input") as HTMLInputElement;

// Equivalent older syntax (not admitted in .tsx):
const len = (<string>someValue).length;

Type assertions do not perform runtime checking; they are erased during compilation. The conventional discipline avoids as where narrowing or generic constraints would suffice.

The double assertion (as unknown as T) admits substantial unsafety; conventionally a code smell:

const x = obj as unknown as SomeType;            // bypass type checking

satisfies

Since TS 4.9, the satisfies operator admits “value is of this type, but preserve its specific type”:

type Config = Record<string, string | number>;

const config = {
    host: "localhost",
    port: 8080,
} satisfies Config;

config.host.toUpperCase();                       // OK; host's type is "localhost", not string | number

The mechanism admits both type checking (the value satisfies the constraint) and type precision (the inferred type is more specific than the constraint). Conventional for configuration objects and the as const pattern.

Enums

The enum keyword introduces an enumeration:

enum Direction {
    Up,                                          // 0
    Down,                                        // 1
    Left,                                        // 2
    Right,                                       // 3
}

let d: Direction = Direction.Up;

// String enums:
enum Status {
    Active = "active",
    Inactive = "inactive",
    Banned = "banned",
}

// const enum (compile-time only):
const enum LogLevel {
    Debug,
    Info,
    Warning,
    Error,
}

The conventional Go-style discipline since TS 5.x is to prefer union literal types over enums:

// Conventional alternative:
type Direction = "Up" | "Down" | "Left" | "Right";
const directions: readonly Direction[] = ["Up", "Down", "Left", "Right"];

The discipline produces simpler runtime semantics — string enums require imports of the enum value, while string literals are just strings.

Type aliases for primitives

Type aliases admit aliasing existing types — including primitives:

type UserId = number;
type EmailAddress = string;

function lookupUser(id: UserId): User { /* ... */ }

The aliases are transparent — they are not distinct types; lookupUser(42) is admitted directly. For nominal types (distinct from the underlying primitive), the conventional defence is branded types:

type UserId = number & { readonly __brand: "UserId" };

function makeUserId(n: number): UserId {
    return n as UserId;
}

function lookupUser(id: UserId) { /* ... */ }

const id = makeUserId(42);
lookupUser(id);                                  // OK
lookupUser(42);                                  // ERROR: not a UserId

The pattern admits substantial type safety for identifier-like values.

keyof, typeof, in

Three type operators:

typeof

Produces the type of a value:

const config = { host: "localhost", port: 8080 };
type Config = typeof config;                     // { host: string; port: number }

function getConfig(): typeof config {
    return config;
}

keyof

Produces the union of property keys:

interface Person {
    name: string;
    age: number;
    email: string;
}

type PersonKey = keyof Person;                   // "name" | "age" | "email"

function get<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

in

Used in mapped types (treated in Advanced types):

type Optional<T> = {
    [K in keyof T]?: T[K];
};

Indexed access types

The T[K] form admits accessing the type of a property:

interface Person {
    name: string;
    age: number;
}

type NameType = Person["name"];                  // string
type AgeType = Person["age"];                    // number

type AllValues = Person[keyof Person];           // string | number

The form admits substantial flexibility for type-level computation.

A note on the conventional discipline

The contemporary TypeScript type advice:

  • Use strict: true — enables the full type-discipline surface.
  • Prefer unknown over any — type-safe alternative.
  • Prefer literal unions over enums — simpler runtime semantics.
  • Use as const for literal-type configuration.
  • Use satisfies for type-checked-but-precise values.
  • Use type aliases for unions and tuples; use interfaces for object shapes.
  • Use readonly for immutable properties.
  • Use Record<K, V> for typed records.
  • Use branded types for nominal type safety.
  • Avoid type assertions — use narrowing or generics.

The combination — primitive types, structural object types, literal unions, intersections, generics, the full type-system surface — is the substance of TypeScript’s type system. The discipline produces substantial compile-time safety without changing the JavaScript runtime semantics.