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:
| Feature | type | interface |
|---|---|---|
| Object types | Yes | Yes |
| Union and intersection | Yes | Only via extends |
| Tuple, primitive aliases | Yes | No |
| Conditional and mapped types | Yes | No |
| Declaration merging | No | Yes |
extends syntax | Via & | Via extends |
The conventional discipline:
- Use
interfacefor object types meant to be implemented or extended. - Use
typefor 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
unknownoverany— type-safe alternative. - Prefer literal unions over enums — simpler runtime semantics.
- Use
as constfor literal-type configuration. - Use
satisfiesfor type-checked-but-precise values. - Use type aliases for unions and tuples; use interfaces for object shapes.
- Use
readonlyfor 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.