Object Types

Everyday Types introduced object types, interface and type. The details below decide whether an object type really protects you: how far readonly reaches, what dictionaries return for missing keys, when extra properties are rejected and how types combine. Every rule is about the shape of a value, never the constructor that made it.

Property modifiers: optional and readonly

readonly stops reassignment of the property itself but is shallow: nested objects stay mutable. A readonly string[] (or ReadonlyArray<string>) removes push, splice and index assignment. It is a compile-time promise only, and assignability ignores it, so a mutable alias can still change the property; use Object.freeze (Descriptors and Freezing) for runtime protection.

Shallow readonly, a mutable alias and an optional propertyCSS
interface Settings {
  readonly theme: { dark: boolean };
  readonly tags: readonly string[];
  fontSize?: number;                                // number | undefined
}
const s: Settings = { theme: { dark: false }, tags: ["ts"] };
s.theme.dark = true;                                // OK: readonly is shallow
s.theme = { dark: true };
s.tags.push("js");
const writable: { theme: { dark: boolean } } = s;   // readonly ignored here
writable.theme = { dark: false };                   // changes s.theme too
s.fontSize = undefined;                             // allowed by default
Output
modifiers.ts(8,3): error TS2540: Cannot assign to 'theme' because it is a read-only property.
modifiers.ts(9,8): error TS2339: Property 'push' does not exist on type 'readonly string[]'.

An optional property may be missing or hold undefined. The exactOptionalPropertyTypes option (not part of strict) forbids writing undefined explicitly and reports error TS2412 on the last line, which matters when code tests "fontSize" in s or merges objects with spread.

Index signatures

When property names are not known in advance, as in a lookup table or parsed JSON, an index signature [key: string]: T describes every property at once; Record<string, T> is the same type as a utility (Utility Types). Keys may be string, number, symbol, template-literal patterns or unions of these. Named properties must fit the signature, and a numeric index type must be assignable to the string one, because JavaScript converts obj[1] to obj["1"].

Index signatures, Record and template-pattern keysCSS
interface HttpHeaders {
  [name: string]: string;              // any string key, string values
  "content-type": string;              // known properties must fit the signature
  retries: number;
}
const stock: Record<string, number> = { apples: 3 };   // same as { [k: string]: number }
const pears = stock.pears;             // number, although no pears exist
console.log(pears.toFixed(0));
type Grid = { [cell: `${string}-${number}`]: boolean };  // template-pattern keys
const grid: Grid = { "A-1": true, B2: false };
Output
index.ts(4,3): error TS2411: Property 'retries' of type 'number' is not assignable to 'string'
  index type 'string'.
index.ts(11,35): error TS2353: Object literal may only specify known properties, and 'B2' does
  not exist in type
'Grid'.

The line that compiles is the dangerous one: stock.pears is typed number but is undefined at runtime, so toFixed throws a TypeError. With noUncheckedIndexedAccess every index read, array elements included, gains | undefined, and tsc reports error TS18048: 'pears' is possibly 'undefined'. For dictionaries that change at runtime, a Map<string, number> is often clearer: its get already returns number | undefined.

Excess property checks

Structural typing normally allows extra properties. The exception is a fresh object literal written directly where a type is expected: an unknown property there is almost certainly a typo. Assigning the literal to a variable first, asserting its type or adding an index signature switches the check off.

Fresh literals are checked, variables are not, satisfies keeps literal typesCSS
interface ButtonOptions {
  label: string;
  color?: "primary" | "danger";
}
declare function button(options: ButtonOptions): HTMLButtonElement;
button({ label: "Save", colour: "primary" });      // typo caught
const opts = { label: "Save", colour: "primary" };
button(opts);                                      // no check: not a fresh literal
const theme = {
  save: { label: "Save", color: "primary" },
  drop: { label: "Delete", color: "danger", icon: "trash" },
} satisfies Record<string, ButtonOptions>;
const c: "primary" = theme.save.color;             // OK: literal type kept
Output
excess.ts(7,25): error TS2561: Object literal may only specify known properties, but 'colour'
  does not exist in type
'ButtonOptions'. Did you mean to write 'color'?
excess.ts(13,45): error TS2353: Object literal may only specify known properties, and 'icon'
  does not exist in type
'ButtonOptions'.

The second call compiles and silently ignores colour. The satisfies operator (TypeScript 4.9) checks a literal, excess properties included, without replacing its inferred type, so theme.save.color stays "primary" and a misspelled theme.sav is an error, which a Record annotation would allow. Prefer it to an annotation for configuration objects.

Extending and intersecting

interface B extends A, C combines one or more types and checks that redeclared properties stay compatible. An intersection A & C builds the same combination but never complains: conflicting properties are intersected, and number & string is never, a type no value has.

Interface extension reports conflicts; intersections produce neverTypeScript
interface Entity { id: number }
interface Timestamped { createdAt: Date }
interface Post extends Entity, Timestamped { title: string }
type Reply = Entity & Timestamped & { body: string };
const reply: Reply = { id: 2, createdAt: new Date(), body: "Nice" };
interface Draft extends Post { id: string }
type Broken = Post & { id: string };               // id: number & string = never
const b: Broken = { id: 3, createdAt: new Date(), title: "x" };
Output
extend.ts(7,11): error TS2430: Interface 'Draft' incorrectly extends interface 'Post'.
  Types of property 'id' are incompatible.
    Type 'string' is not assignable to type 'number'.
extend.ts(9,21): error TS2322: Type 'number' is not assignable to type 'never'.

The interface reports the mistake where you made it; the intersection defers it to a puzzling error at first use. The TypeScript team's performance guide also favors extends over large intersections, because the compiler caches relationships between named interfaces.

Choosing between interface and type
Feature interface type alias
Unions, tuples, primitives, mapped types No Yes
Combining object types extends: conflicts are errors &: conflicts become never
Declaration merging Yes, reopen to add members No, duplicate identifier
implements in a class Yes Object types only, not unions
Generic object types, readonly arrays and tuples

Object types take type parameters too: interface Box<T> { contents: T } works like Array<T> or Map<K, V> (Generics). A tuple is an array with a fixed length and a type per position. Elements can be labeled, optional (only at the end, making length a union) or rest elements, and as const infers a readonly tuple.

Tuples and readonly array parametersTypeScript
type Span = [start: number, end: number, step?: number];    // labeled, optional
type Row = [id: number, ...cells: string[]];                // rest element
const r: Span = [0, 10];
const row: Row = [7, "Ada", "Lovelace"];
const len: 2 | 3 = r.length;
const point = [3, 4] as const;                              // readonly [3, 4]
function total(values: readonly number[]) {
  values[0] = 1;
}
const short: Span = [1];
Output
tuple.ts(9,3): error TS2542: Index signature in type 'readonly number[]' only permits reading.
tuple.ts(11,7): error TS2322: Type '[number]' is not assignable to type 'Span'.
  Source has 1 element(s) but target requires 2.

Declare readonly T[] parameters in functions that only read: a mutable array is assignable to them but not the reverse, so callers can pass either. Keep tuples for short positional data such as [key, value] entries; beyond three elements, an object with named properties reads better.