bastianplsfix

Object types

An object type describes a shape, and it matches every value that has that shape. Nothing declares that it implements one: you can write the type after the value it describes and it still matches, and a value carrying more properties than the type asks for matches too. That second half has exactly one exception, a fresh object literal written directly into a typed position, refused for having a property the type does not mention. Most confusion about object types is that exception, so this entry spends real time on it.

Underneath, a type is a list of members, and a member is one of five things. This entry is about what each of them can say and what the type therefore refuses. The objects page is the entry for the values; this one is about the descriptions.

Create programs/object-types.test.ts for this reference and keep it open. Start it with the assertion helpers used on this page:

import { assert, assertEquals, assertStrictEquals } from "@std/assert";

Below the import, add one exported type the page refers back to, holding all five kinds of member in one place:

export type Everything = {
property: boolean;
method(input: string): number;
[key: string]: unknown;
(input: number): string;
new (input: string): { name: string };
};

A property signature, a method signature, an index signature, a call signature, and a construct signature. The first two fill this page, index signatures get their own steps below, and the last two describe functions and constructors, which belong to the functions and classes pages. The parameter names in those signatures are documentation and nothing else: calling the parameter input or key changes no behaviour, and key cannot be omitted even though it means nothing. The export keeps the linter satisfied that a type used by no test is still deliberate.

four members and three modifiers

Deno.test("four members and three modifiers", () => {
type Product = {
readonly sku: string;
name: string;
discount?: number;
price(quantity: number): number;
};

const carrot: Product = {
sku: "veg-1",
name: "carrot",
price(quantity) {
return quantity * 40;
},
};

assertStrictEquals(carrot.price(3), 120);
assertStrictEquals(carrot.discount, undefined);
});
Check programs/object-types.test.ts
running 1 test from ./programs/object-types.test.ts
four members and three modifiers ... ok (201µs)

ok | 1 passed | 0 failed (2ms)

sku is a property signature that cannot be assigned to after the object exists, name is an ordinary one, discount is optional so a value may leave it out, and price is a method signature. Commas and semicolons both work as separators, with a trailing one allowed; pick one and stay with it. And note that price(quantity) needs no annotation, because the annotation on carrot supplies one. That is contextual typing, and it is the reason a well-typed boundary makes the code inside it quieter rather than noisier.

a method and a function-valued property are the same type

Deno.test("a method and a function-valued property are the same type", () => {
type HasMethod = { format(value: number): string };
type HasFunctionProperty = { format: (value: number) => string };

const method: HasMethod = { format: (value) => value.toFixed(1) };
const property: HasFunctionProperty = method;
const backAgain: HasMethod = property;

assertStrictEquals(backAgain.format(1), "1.0");
assertStrictEquals(backAgain.format, method.format);
});
a method and a function-valued property are the same type ... ok (48µs)

One value, assigned back and forth between the two types with no complaint from either direction, and still the same function at the end. To the checker there is no difference, so the choice is about how the property will be written rather than about what it means: the method shorthand for something a class or object literal will define as a method, and the arrow form for a property that happens to hold a function.

the type can be written after the value

Deno.test("the type can be written after the value", () => {
const measured = { width: 3, height: 4, unit: "cm" };

type Rectangle = { width: number; height: number };

function area(rectangle: Rectangle): number {
return rectangle.width * rectangle.height;
}

assertStrictEquals(area(measured), 12);
assertStrictEquals(area({ width: 2, height: 5 }), 10);
});
the type can be written after the value ... ok (30µs)

measured was written before Rectangle existed and knows nothing about it. It matches anyway, because matching is a question about structure and not about history, which is why a type can be added to code that already works, and the single largest difference from a language where a type must be declared and implemented. The extra unit is fine too: area asked for two numbers, got them, and has no business objecting to the rest.

the exception is a fresh literal

Hand the same shape over as a literal instead of a variable:

Deno.test("the exception is a fresh literal", () => {
type Rectangle = { width: number; height: number };

const written: Rectangle = { width: 3, height: 4, unit: "cm" };
assertStrictEquals(written.width * written.height, 12);
});
Check programs/object-types.test.ts
TS2353 [ERROR]: Object literal may only specify known properties, and 'unit' does not exist in type 'Rectangle'.
const written: Rectangle = { width: 3, height: 4, unit: "cm" };
~~~~
at file:///programs/object-types.test.ts:62:53

error: Type checking failed.

The same shape that was welcome as a variable is refused as a literal. Structurally the literal is a perfectly good Rectangle and the assignment is sound, which is why the shielded version below runs, so the check is not part of the type system's logic. It is a heuristic about freshness: an object you wrote inline, right there, with a property nothing wants, is almost always a typo or a field that used to exist. Shield both refusals, noting the check reaches inside an array literal too:

Deno.test("the exception is a fresh literal", () => {
type Rectangle = { width: number; height: number };

// @ts-expect-error: Object literal may only specify known properties, and 'unit' does not exist in type 'Rectangle'.
const written: Rectangle = { width: 3, height: 4, unit: "cm" };

// @ts-expect-error: Object literal may only specify known properties, and 'unit' does not exist in type 'Rectangle'.
const inAnArray: Rectangle[] = [{ width: 3, height: 4, unit: "cm" }];

assertStrictEquals(written.width * written.height, 12);
assertStrictEquals(inAnArray.length, 1);
});
the exception is a fresh literal ... ok (30µs)

The second error code says the justification out loud. Put a typo against an optional property in a scratch file programs/excess-properties.ts:

type Person = {
first: string;
middle?: string;
last: string;
};

export function fullName(person: Person): string {
return [person.first, person.middle, person.last].filter(Boolean).join(" ");
}

export const ada = fullName({
first: "Ada",
mdidle: "Cecily",
last: "Lovelace",
});
Check programs/excess-properties.ts
TS2561 [ERROR]: Object literal may only specify known properties, but 'mdidle' does not exist in type 'Person'. Did you mean to write 'middle'?
mdidle: "Cecily",
~~~~~~
at file:///programs/excess-properties.ts:13:3

error: Type checking failed.

middle is optional, so a misspelling of it looks exactly like leaving it out and supplying something else. Without the excess property check that call would be silently accepted and Ada would lose her middle name; with it, TS2561 names the property you meant. That is the whole justification for the rule, and it is worth knowing, because the rule is otherwise indefensible: it refuses assignments the type system considers correct. Delete the scratch file.

four escapes, and the one to reach for

Deno.test("four escapes, and the one to reach for", () => {
type Rectangle = { width: number; height: number };
type RectangleEtc = Rectangle & { [key: string]: unknown };

function area(rectangle: Rectangle): number {
return rectangle.width * rectangle.height;
}

function areaOf<R extends Rectangle>(rectangle: R): number {
return rectangle.width * rectangle.height;
}

const viaVariable = { width: 3, height: 4, unit: "cm" };
const viaAssertion = { width: 3, height: 4, unit: "cm" } as Rectangle;
const viaSignature: RectangleEtc = { width: 3, height: 4, unit: "cm" };

assertStrictEquals(area(viaVariable), 12);
assertStrictEquals(area(viaAssertion), 12);
assertStrictEquals(areaOf({ width: 3, height: 4, unit: "cm" }), 12);
assertStrictEquals(area(viaSignature), 12);
});
four escapes, and the one to reach for ... ok (36µs)

Four ways through, in the order they should occur to you. The variable is the one to reach for: the literal is no longer fresh, so the heuristic stops applying, and nothing about the types was weakened to get there. The assertion works and costs you the check on everything else in that literal, since an assertion permits any shape that overlaps enough, so the typo you were being protected from can come back. The type parameter works because a parameter of type R is inferred from the argument, meaning there is no assignment to a declared shape and nothing to be fresh against; use it when you genuinely want to keep the caller's full type, not as a way to silence a message. And the index signature works by changing the type's meaning permanently, accepting every property with every name, which is right when the shape really is open and wrong when you have one awkward call site.

an assertion can silence the wrong half

The assertion's cost deserves its own demonstration. A factory returns something with private working state it cannot declare, in a scratch file programs/incrementor.ts:

type Incrementor = {
inc(): number;
};

export function createIncrementor(): Incrementor {
return {
counter: 0,
inc() {
return this.counter++;
},
};
}
Check programs/incrementor.ts
TS2353 [ERROR]: Object literal may only specify known properties, and 'counter' does not exist in type 'Incrementor'.
counter: 0,
~~~~~~~
at file:///programs/incrementor.ts:7:5

TS2339 [ERROR]: Property 'counter' does not exist on type 'Incrementor'.
return this.counter++;
~~~~~~~
at file:///programs/incrementor.ts:9:19

Found 2 errors.

error: Type checking failed.

Two errors already, because the return type decided what this is inside inc as well. Reach for the assertion, as the fresh-literal reflex suggests, still in the scratch file:

type Incrementor = {
inc(): number;
};

export function createIncrementor(): Incrementor {
return {
counter: 0,
inc() {
return this.counter++;
},
} as Incrementor;
}
Check programs/incrementor.ts
TS2339 [ERROR]: Property 'counter' does not exist on type 'Incrementor'.
return this.counter++;
~~~~~~~
at file:///programs/incrementor.ts:9:19

error: Type checking failed.

The excess property complaint is gone and the worse one remains. as Incrementor decided what the whole literal is, including what this is inside inc, so the method cannot see the field sitting next to it: an assertion is not a note about one property, it is a statement about the entire value. The variable fixes both at once, because inference gives the intermediate object a type with counter on it, inc can see it, and the return statement narrows to Incrementor on the way out. Delete the scratch file, and keep the working shape:

Deno.test("an assertion can silence the wrong half", () => {
type Incrementor = { inc(): number };

function createIncrementor(): Incrementor {
const state = {
counter: 0,
inc() {
return this.counter++;
},
};
return state;
}

const incrementor = createIncrementor();
assertStrictEquals(incrementor.inc(), 0);
assertStrictEquals(incrementor.inc(), 1);
});
an assertion can silence the wrong half ... ok (30µs)

That is the whole argument for preferring the boring escape.

satisfies checks a literal without retyping it

Deno.test("satisfies checks a literal without retyping it", () => {
type Rate = { small: number; large: number };

const rates = { small: 1, large: 2 } satisfies Rate;

// @ts-expect-error: Object literal may only specify known properties, and 'medium' does not exist in type 'Rate'.
const typo = { small: 1, large: 2, medium: 3 } satisfies Rate;

// @ts-expect-error: Property 'large' is missing in type '{ small: number; }' but required in type 'Rate'.
const short = { small: 1 } satisfies Rate;

assertStrictEquals(typo.medium, 3);
assertEquals(Object.keys(rates), ["small", "large"]);
assertStrictEquals(short.small, 1);

const labels = { small: "S", large: "L" } as const satisfies Record<
keyof Rate,
string
>;
const widened = { small: "S", large: "L" } satisfies Record<
keyof Rate,
string
>;

const kept: "S" = labels.small;
// @ts-expect-error: Type 'string' is not assignable to type '"S"'.
const lost: "S" = widened.small;

assertStrictEquals(kept, lost);
});
satisfies checks a literal without retyping it ... ok (176µs)

satisfies asks the checker to confirm that a value matches a type, and then leaves the value's own type alone, and both halves matter. It applies the excess property check, so the typo protection is intact; it enforces the type's requirements, so a missing property is still an error; and it does not replace the value's type with the type it checked against, which is why typo.medium is still reachable where an annotation would have erased it. The catch, and it surprises people: satisfies supplies a contextual type, which is enough to widen "S" into string, as the shielded lost line shows. So satisfies alone gives you the check without the literal types, and as const satisfies gives you both, which is the spelling for a table whose keys are checked and whose values stay exact; the last assertion confirms the two values are identical at run time, the difference living entirely in the types.

optional and undefined answer different questions

Deno.test("optional and undefined answer different questions", () => {
type Options = { retries?: number; timeout: number | undefined };

const omitted: Options = { timeout: undefined };
const explicit: Options = { retries: undefined, timeout: 1_000 };

assert(!("retries" in omitted));
assert("retries" in explicit);
assert("timeout" in omitted);

assertStrictEquals(omitted.retries, explicit.retries);
});
optional and undefined answer different questions ... ok (22µs)

retries?: number says the property may be absent. timeout: number | undefined says the property must be there and may hold nothing, which is useful precisely because it forces a decision to be written down: a reader seeing timeout: undefined knows the option exists and was switched off. Reading them apart takes in, since both give undefined from a property access, the same point the objects page makes from the value's side and the nothing, twice page from the undefined side. By default the two overlap in one direction, since an optional property also accepts an explicit undefined, which is why explicit type-checks; the exactOptionalPropertyTypes compiler option separates them, turning that line into TS2375. It is off by default, in Deno as in tsc. Turn it on in new code, and expect turning it on in old code to find real bugs and a great deal of noise, because passing undefined to mean leave-it-out is a common habit.

an index signature is a member too

Two spellings the checker refuses outright, in a scratch file programs/index-keys.ts:

export type ByTwoNames = { [key: "width" | "height"]: boolean };

export type Mixed = {
[key: string]: boolean;
name: string;
};
Check programs/index-keys.ts
TS1337 [ERROR]: An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.
export type ByTwoNames = { [key: "width" | "height"]: boolean };
~~~
at file:///programs/index-keys.ts:1:29

TS2411 [ERROR]: Property 'name' of type 'string' is not assignable to 'string' index type 'boolean'.
name: string;
~~~~
at file:///programs/index-keys.ts:5:3

Found 2 errors.

error: Type checking failed.

Four key types are legal, string, number, symbol, and a template literal, plus unions of those, and the rule underneath is that the key type must describe infinitely many keys: `${bigint}` expands to every digit string and is allowed, where "width" | "height" is finite and gets TS1337, whose message points at the right tool, since Record<"width" | "height", boolean> is how you spell a finite key set. And the second error follows from what a signature claims: if every string key holds a boolean, and name is a string key, then name holds a boolean, so declaring otherwise is a contradiction rather than a special case. Delete the scratch file and pin the legal shapes:

Deno.test("an index signature is a member too", () => {
type Flags = { [key: string]: boolean };
const flags: Flags = { a: true, b: false };
assertStrictEquals(flags.a, true);
assertStrictEquals(flags.missing, undefined);

type ByDigits = { [key: `${bigint}`]: boolean };
const digits: ByDigits = { "12": true };
assertStrictEquals(digits["12"], true);
});
an index signature is a member too ... ok (19µs)

flags.missing is typed boolean and holds undefined, the index-signature optimism the objects as dictionaries page measures at length.

a number key is a string key

0 and "0" look like different keys. Predict the string read:

Deno.test("a number key is a string key", () => {
type Registry = {
[key: string]: object;
[key: number]: RegExp;
};

const registry: Registry = { 0: /a/, name: {} };

assertStrictEquals(registry["0"], undefined);
});
Check programs/object-types.test.ts
running 10 tests from ./programs/object-types.test.ts
...
a number key is a string key ... FAILED (8ms)

ERRORS

a number key is a string key => ./programs/object-types.test.ts:173:6
error: AssertionError: Values are not strictly equal.

[Diff] Actual / Expected

- /a/
+ undefined

FAILURES

a number key is a string key => ./programs/object-types.test.ts:173:6

FAILED | 9 passed | 1 failed (11ms)

error: Test failed

One slot, reached two ways. The reason is not a rule about index signatures but a rule about JavaScript: every property key is a string or a symbol, so a numeric key is converted before it is used, and registry[0] and registry["0"] are the same entry, which Object.keys reports as "0". The two signatures coexist because RegExp is assignable to object; reverse them and you get TS2413, since the number signature describes a subset of what the string signature describes, and a subset must not promise something the superset contradicts. Correct the prediction:

Deno.test("a number key is a string key", () => {
type Registry = {
[key: string]: object;
[key: number]: RegExp;
};

const registry: Registry = { 0: /a/, name: {} };

assertEquals(registry[0], /a/);
assertEquals(registry["0"], /a/);
assertEquals(Object.keys(registry), ["0", "name"]);
});
a number key is a string key ... ok (43µs)

Record and an index signature differ in exactly one place

Deno.test("Record and an index signature differ in exactly one place", () => {
const fromSignature: keyof { [key: string]: number } = 1;

// @ts-expect-error: Type 'number' is not assignable to type 'string'.
const fromRecord: keyof Record<string, number> = 1;

assertStrictEquals(fromSignature, fromRecord);
});
Record and an index signature differ in exactly one place ... ok (10µs)

Record<string, number> and {[key: string]: number} are the same type by every test worth running, each assignable to the other, and then keyof disagrees: keyof {[key: string]: number} is string | number, because a numeric key is usable there, as the previous step showed, where keyof Record<string, number> is string alone, because Record<K, V> is a mapped type, a transform over the keys in K, so keyof gives back exactly K. This matters the moment a generic function is constrained by keyof T, and it is worth knowing simply because two spellings usually presented as interchangeable are not.

never forbids what a type would otherwise allow

Deno.test("never forbids what a type would otherwise allow", () => {
type NoProperties = Record<PropertyKey, never>;
type NotArrayLike = Record<number, never> & { name?: string };

const nothing: NoProperties = {};

// @ts-expect-error: Type 'number' is not assignable to type 'never'.
const something: NoProperties = { anything: 1 };

const named: NotArrayLike = { name: "carrot" };

// @ts-expect-error: Type 'string' is not assignable to type 'never'.
const indexed: NotArrayLike = ["carrot"];

assertEquals(Object.keys(nothing), []);
assertEquals(Object.keys(something), ["anything"]);
assertStrictEquals(named.name, "carrot");
assertEquals(Object.keys(indexed), ["0"]);
});
never forbids what a type would otherwise allow ... ok (35µs)

Nothing is assignable to never, which the any, unknown, never page explains as the empty set, so a key mapped to never is a key you cannot supply a value for, and mapping every key to never gives the type that genuinely means no properties. Record<number, never> is the same trick aimed at numeric keys only, making an object with named properties legal and an array illegal, useful when a function takes a configuration object and an array would be a silent mistake. Note what Record<PropertyKey, never> is not: it is not {}, which brings us to the four types people reach for when they mean some object.

the four general object types

Two of the four spellings are refused where you write them. Put them in a scratch file programs/general-types.ts and lint it:

export const nearlyAnything: {} = 123;
export const instanceOfObject: Object = 123;
error[ban-types]: `{}` doesn't mean an empty object, but means any types other than `null` and `undefined`
--> programs/general-types.ts:1:30
|
1 | export const nearlyAnything: {} = 123;
| ^^
= hint: If you want a type that means "empty object", use `Record<PropertyKey, never>` instead

docs: https://docs.deno.com/lint/rules/ban-types


error[ban-types]: This type may be different from what you expect it to be
--> programs/general-types.ts:2:32
|
2 | export const instanceOfObject: Object = 123;
| ^^^^^^
= hint: If you want a type meaning "any object", use `object` instead. Or if you want a type meaning "any value", you probably want `unknown` instead.

docs: https://docs.deno.com/lint/rules/ban-types


Found 2 problems
Checked 1 file

Note what the checker did not do: both lines type-check, because 123 genuinely inhabits both types. {} is not the empty object type but very nearly the top type, accepting everything except null and undefined. Object, capital O, sounds narrower and is much the same, admitting every primitive except those two because a primitive borrows Object.prototype's methods through its wrapper, while also policing those methods, so { toString: true } is refused where {} accepts it. ban-types is on by default and refuses both spellings with a hint naming the one you meant, so the taxonomy is something you can forget and rediscover from your editor. Delete the scratch file and keep the honest pair:

Deno.test("the four general object types", () => {
const nonPrimitive: object = { a: 1 };

// @ts-expect-error: Type 'number' is not assignable to type 'object'.
const notANumber: object = 123;

assertEquals(nonPrimitive, { a: 1 });
assertStrictEquals(notANumber, 123);
});
the four general object types ... ok (17µs)

object is every non-primitive value, the honest some-object type, and Record<PropertyKey, never> from the previous step is the honest empty one. Reach for object when you mean any object, unknown when you mean any value, and Record<PropertyKey, never> when you mean no properties, remembering that none of the three are much use directly, since you cannot read a property off any of them without the narrowing the unions and narrowing page covers.

an inherited member counts as a member

Describable asks for two members, and thing declares one. It type-checks, so predict what Object.hasOwn says about the other:

Deno.test("an inherited member counts as a member", () => {
type Describable = { toString(): string; label: string };

const thing: Describable = { label: "carrot" };

assertStrictEquals(String(thing), "[object Object]");
assertStrictEquals(Object.hasOwn(thing, "toString"), true);
assert("toString" in thing);
});
Check programs/object-types.test.ts
running 14 tests from ./programs/object-types.test.ts
...
an inherited member counts as a member ... FAILED (8ms)

ERRORS

an inherited member counts as a member => ./programs/object-types.test.ts:229:6
error: AssertionError: Values are not strictly equal.

[Diff] Actual / Expected

- false
+ true

FAILURES

an inherited member counts as a member => ./programs/object-types.test.ts:229:6

FAILED | 13 passed | 1 failed (10ms)

error: Test failed

Not an own property. The toString is inherited from Object.prototype, and TypeScript makes no distinction between an own member and an inherited one: both are simply members, so the one-property literal satisfies the two-member type. That is a real simplification with a real cost, because some facts about JavaScript objects cannot be written down in the type system, and the difference it drops is exactly the one the prototypes and inheritance page is about and that Object.hasOwn exists to answer. It also explains a smaller thing: every object literal type describes an instance of Object, so a value typed {} already has toString. Correct the prediction to false:

an inherited member counts as a member ... ok (13µs)

satisfies at a definition site, an annotation at a boundary

Deno.test("satisfies at a definition site, an annotation at a boundary", () => {
type Options = { retries?: number; timeout?: number };

const DEFAULTS = { retries: 3, timeout: 1_000 } satisfies Required<Options>;

function withDefaults(options: Options): Required<Options> {
return { ...DEFAULTS, ...options };
}

assertEquals(withDefaults({}), { retries: 3, timeout: 1000 });
assertEquals(withDefaults({ retries: 5 }), { retries: 5, timeout: 1000 });
});
satisfies at a definition site, an annotation at a boundary ... ok (25µs)

The constant is checked against the type without being flattened into it, so the values stay as precise as they were written. The function's parameter and return type are annotations, because that is where a promise to a caller belongs.

The whole entry

Run the whole reference suite:

Check programs/any-unknown-never.test.ts
Check programs/arrays.test.ts
Check programs/assignment.test.ts
Check programs/async-functions.test.ts
Check programs/async-iteration.test.ts
Check programs/branching.test.ts
Check programs/buffers-and-views.test.ts
Check programs/classes.test.ts
Check programs/closures.test.ts
Check programs/conversion-and-coercion.test.ts
Check programs/dates-and-times.test.ts
Check programs/designing-error-types.test.ts
Check programs/destructuring.test.ts
Check programs/equality.test.ts
Check programs/errors-and-exceptions.test.ts
Check programs/functions.test.ts
Check programs/generators.test.ts
Check programs/iterables-and-iterators.test.ts
Check programs/iterator-helpers.test.ts
Check programs/json.test.ts
Check programs/loops.test.ts
Check programs/maps.test.ts
Check programs/matching-and-replacing.test.ts
Check programs/module-specifiers.test.ts
Check programs/modules.test.ts
Check programs/mutating-arrays.test.ts
Check programs/nothing-twice.test.ts
Check programs/numbers.test.ts
Check programs/object-types.test.ts
Check programs/objects-as-dictionaries.test.ts
Check programs/objects.test.ts
Check programs/ordering-and-sorting.test.ts
Check programs/parameters-and-arguments.test.ts
Check programs/private-class-members.test.ts
Check programs/promise-combinators.test.ts
Check programs/promises.test.ts
Check programs/prototypes-and-inheritance.test.ts
Check programs/read-only.test.ts
Check programs/regular-expressions.test.ts
Check programs/scope-and-declarations.test.ts
Check programs/sentinels.test.ts
Check programs/sets.test.ts
Check programs/strings.test.ts
Check programs/subclassing.test.ts
Check programs/symbols.test.ts
Check programs/tagged-templates.test.ts
Check programs/text-and-characters.test.ts
Check programs/the-event-loop.test.ts
Check programs/the-value-of-this.test.ts
Check programs/transforming-arrays.test.ts
Check programs/truthiness.test.ts
Check programs/typed-arrays.test.ts
Check programs/unicode-in-patterns.test.ts
Check programs/unions-and-narrowing.test.ts
Check programs/values-and-references.test.ts
Check programs/weak-collections.test.ts
Check programs/what-a-type-is.test.ts
running 10 tests from ./programs/any-unknown-never.test.ts
...
running 13 tests from ./programs/arrays.test.ts
...
running 9 tests from ./programs/assignment.test.ts
...
running 10 tests from ./programs/async-functions.test.ts
...
running 11 tests from ./programs/async-iteration.test.ts
...
running 10 tests from ./programs/branching.test.ts
...
running 12 tests from ./programs/buffers-and-views.test.ts
...
running 11 tests from ./programs/classes.test.ts
...
running 6 tests from ./programs/closures.test.ts
...
running 11 tests from ./programs/conversion-and-coercion.test.ts
...
running 13 tests from ./programs/dates-and-times.test.ts
...
running 10 tests from ./programs/designing-error-types.test.ts
...
running 14 tests from ./programs/destructuring.test.ts
...
running 11 tests from ./programs/equality.test.ts
...
running 10 tests from ./programs/errors-and-exceptions.test.ts
...
running 11 tests from ./programs/functions.test.ts
...
running 12 tests from ./programs/generators.test.ts
...
running 14 tests from ./programs/iterables-and-iterators.test.ts
...
running 12 tests from ./programs/iterator-helpers.test.ts
...
running 11 tests from ./programs/json.test.ts
...
running 14 tests from ./programs/loops.test.ts
...
running 15 tests from ./programs/maps.test.ts
...
running 15 tests from ./programs/matching-and-replacing.test.ts
...
running 6 tests from ./programs/module-specifiers.test.ts
...
running 12 tests from ./programs/modules.test.ts
...
running 10 tests from ./programs/mutating-arrays.test.ts
...
running 11 tests from ./programs/nothing-twice.test.ts
...
running 15 tests from ./programs/numbers.test.ts
...
running 15 tests from ./programs/object-types.test.ts
four members and three modifiers ... ok (174µs)
a method and a function-valued property are the same type ... ok (35µs)
the type can be written after the value ... ok (23µs)
the exception is a fresh literal ... ok (14µs)
four escapes, and the one to reach for ... ok (27µs)
an assertion can silence the wrong half ... ok (23µs)
satisfies checks a literal without retyping it ... ok (165µs)
optional and undefined answer different questions ... ok (20µs)
an index signature is a member too ... ok (13µs)
a number key is a string key ... ok (34µs)
Record and an index signature differ in exactly one place ... ok (14µs)
never forbids what a type would otherwise allow ... ok (41µs)
the four general object types ... ok (17µs)
an inherited member counts as a member ... ok (12µs)
satisfies at a definition site, an annotation at a boundary ... ok (24µs)
running 14 tests from ./programs/objects-as-dictionaries.test.ts
...
running 13 tests from ./programs/objects.test.ts
...
running 12 tests from ./programs/ordering-and-sorting.test.ts
...
running 11 tests from ./programs/parameters-and-arguments.test.ts
...
running 11 tests from ./programs/private-class-members.test.ts
...
running 11 tests from ./programs/promise-combinators.test.ts
...
running 11 tests from ./programs/promises.test.ts
...
running 12 tests from ./programs/prototypes-and-inheritance.test.ts
...
running 12 tests from ./programs/read-only.test.ts
...
running 13 tests from ./programs/regular-expressions.test.ts
...
running 9 tests from ./programs/scope-and-declarations.test.ts
...
running 8 tests from ./programs/sentinels.test.ts
...
running 13 tests from ./programs/sets.test.ts
...
running 10 tests from ./programs/strings.test.ts
...
running 11 tests from ./programs/subclassing.test.ts
...
running 10 tests from ./programs/symbols.test.ts
...
running 8 tests from ./programs/tagged-templates.test.ts
...
running 10 tests from ./programs/text-and-characters.test.ts
...
running 9 tests from ./programs/the-event-loop.test.ts
...
running 10 tests from ./programs/the-value-of-this.test.ts
...
running 13 tests from ./programs/transforming-arrays.test.ts
...
running 9 tests from ./programs/truthiness.test.ts
...
running 14 tests from ./programs/typed-arrays.test.ts
...
running 11 tests from ./programs/unicode-in-patterns.test.ts
...
running 13 tests from ./programs/unions-and-narrowing.test.ts
...
running 13 tests from ./programs/values-and-references.test.ts
...
running 9 tests from ./programs/weak-collections.test.ts
...
running 7 tests from ./programs/what-a-type-is.test.ts
...

ok | 641 passed | 0 failed (1s)

Fifteen tests, and the practice is short. Describe the shape where data enters, and let inference carry it inward, because a type on a boundary function pays for itself twice: it checks the caller and supplies contextual types to everything written inside. Use satisfies at a definition site and an annotation at a boundary. When the excess property check is in your way, use a variable, the only escape that weakens nothing. Prefer object to Object and Record<PropertyKey, never> to {}, which the linter will make you do anyway. Turn on exactOptionalPropertyTypes in anything new. Use Record<K, V> for a finite key set and an index signature for an open one, remembering the two are not interchangeable once keyof is involved. And remember that every modifier here is gone at run time, constraining the code the checker can see rather than protecting anything, which the typing classes page makes its subject, alongside the interfaces and type aliases page, the second way to write everything above.