HomepageExploring TypeScript (TS 5.8 Edition)
You can support this book: buy it or donate
(Ad, please don’t block.)

18 Typing objects

In this chapter, we will explore how objects and properties are typed statically in TypeScript.

18.1 Object types

18.1.1 The two ways of using objects

There are two ways of using objects in JavaScript:

Note that the two ways can also be mixed: Some objects are both fixed-layout objects and dictionary objects.

The most common ways of typing these two kinds of objects are:

type FixedLayoutObjectType = {
    product: string,
    quantity: number,
};
type DictionaryObjectType = Record<string, number>;

Next, we’ll look at fixed-layout object types in more detail before coming back to dictionary object types.

18.1.2 Object types work structurally in TypeScript

Object types work structurally in TypeScript: They match all values that have their structure. Therefore, a type can be defined after a given value and still match it – e.g.:

const myPoint = {x: 1, y: 2};

function logPoint(point: {x: number, y: number}): void {
  console.log(point);
}

logPoint(myPoint); // Works!

For more information on this topic, see “Nominal type systems vs. structural type systems” (§13.4).

18.2 Members of object literal types

The constructs inside the bodies of object literal types are called their members. These are the most common members:

type ExampleObjectType = {
  // Property signature
  myProperty: boolean,

  // Method signature
  myMethod(str: string): number,

  // Index signature
  [key: string]: any,

  // Call signature
  (num: number): string,

  // Construct signature
  new(str: string): ExampleInstanceType, 
};

type ExampleInstanceType = {};

Let’s look at these members in more detail:

18.2.1 Method signatures

As far as TypeScript’s type system is concerned, method definitions and properties whose values are functions, are equivalent:

type HasMethodDef = {
  simpleMethod(flag: boolean): void,
};
type HasFuncProp = {
  simpleMethod: (flag: boolean) => void,
};
type _ = Assert<Equal<
  HasMethodDef,
  HasFuncProp
>>;

const objWithMethod = {
  simpleMethod(flag: boolean): void {},
};
assertType<HasMethodDef>(objWithMethod);
assertType<HasFuncProp>(objWithMethod);

const objWithOrdinaryFunction: HasMethodDef = {
  simpleMethod: function (flag: boolean): void {},
};
assertType<HasMethodDef>(objWithOrdinaryFunction);
assertType<HasFuncProp>(objWithOrdinaryFunction);

const objWithArrowFunction: HasMethodDef = {
  simpleMethod: (flag: boolean): void => {},
};
assertType<HasMethodDef>(objWithArrowFunction);
assertType<HasFuncProp>(objWithArrowFunction);

My recommendation is to use whichever syntax best expresses how a property should be set up.

18.2.2 Keys of object type members

18.2.2.1 Quoted keys

Just like in JavaScript, property keys can be quoted:

type Obj = { 'hello everyone!': string };
18.2.2.2 Unquoted numbers as keys

This rarely matters in practice, but as an aside: Just like in JavaScript, we can use unquoted numbers as keys. Unlike JavaScript, those keys are considered to be number literal types:

type _ = Assert<Equal<
  keyof {0: 'a', 1: 'b'},
  0 | 1
>>;

For comparison, this is how JavaScript works:

assert.deepEqual(
  Object.keys({0: 'a', 1: 'b'}),
  [ '0', '1' ]
);

For more information see $type.

18.2.2.3 Computed property keys

Computed property keys are a JavaScript feature. There is a similar feature at the type level:

type ExampleObjectType = {
  // Property signature with computed key
  [Symbol.toStringTag]: string,

  // Method signature with computed key
  [Symbol.iterator](): IteratorObject<string>,
};

Unexpectedly, computed property keys are values, not types. TypeScript internally applies typeof to create the type:

type _ = Assert<Equal<
  { ['hello']: string },
  { hello: string }
>>;

What kind of value is allowed as a computed property key? Its type must be:

18.2.3 Modifiers of object type members

18.2.3.1 Optional properties

If we put a question mark (?) after the name of a property, that property is optional. The same syntax is used to mark parameters of functions, methods, and constructors as optional. In the following example, property .middle is optional:

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

Therefore, it’s OK to omit that property (line A):

const john: Name = {first: 'Doe', last: 'Doe'}; // (A)
const jane: Name = {first: 'Jane', middle: 'Cecily', last: 'Doe'};
18.2.3.1.1 Optional vs. undefined | string with exactOptionalPropertyTypes

In this book, all code uses the compiler setting exactOptionalPropertyTypes. With that setting, the difference an optional property and a property with type undefined | string is intuitive:

type Obj = {
  prop1?: string;
  prop2: undefined | string; 
};

const obj1: Obj = { prop1: 'a', prop2: 'b' };

// .prop1 can be omitted; .prop2 can be `undefined`
const obj2: Obj = { prop2: undefined };

// @ts-expect-error: Type '{ prop1: undefined; prop2: string; }' is not
// assignable to type 'Obj' with 'exactOptionalPropertyTypes: true'.
// Consider adding 'undefined' to the types of the target's properties.
// Types of property 'prop1' are incompatible. Type 'undefined' is not
// assignable to type 'string'.
const obj3: Obj = { prop1: undefined, prop2: 'b' };

// @ts-expect-error: Property 'prop2' is missing in type '{ prop1: string;
// }' but required in type 'Obj'.
const obj4: Obj = { prop1: 'a' };

Types such as undefined | string and null | string are useful if we want to make omissions explicit. When people see such an explicitly omitted property, they know that it exists but was switched off.

18.2.3.1.2 Optional vs. undefined | string without exactOptionalPropertyTypes

If exactOptionalPropertyTypes is false then one thing changes: .prop1 can also be undefined:

type Obj = {
  prop1?: string;
  prop2: undefined | string; 
};

const obj1: Obj = { prop1: undefined, prop2: undefined };
18.2.3.2 Read-only properties

In the following example, property .prop is read-only:

type MyObj = {
  readonly prop: number;
};

As a consequence, we can read it, but we can’t change it:

const obj: MyObj = {
  prop: 1,
};

console.log(obj.prop); // OK

// @ts-expect-error: Cannot assign to 'prop' because it is a read-only
// property.
obj.prop = 2;

18.3 Excess property checks: When are extra properties allowed?

As an example, consider the following object literal type:

type Point = {
  x: number,
  y: number,
};

There are two ways (among others) in which this object literal type could be interpreted:

TypeScript uses both interpretations. To explore how that works, we will use the following function:

function computeDistance(point: Point) { /*...*/ }

The default is that the excess property .z is allowed:

const obj = { x: 1, y: 2, z: 3 };
computeDistance(obj); // OK

However, if we use object literals directly, then excess properties are forbidden:

// @ts-expect-error: Object literal may only specify known properties, and
// 'z' does not exist in type 'Point'.
computeDistance({ x: 1, y: 2, z: 3 }); // error

computeDistance({x: 1, y: 2}); // OK

18.3.1 Why are excess properties forbidden in object literals?

Why the stricter rules for object literals? They provide protection against typos in property keys. We will use the following object literal type to demonstrate what that means.

type Person = {
  first: string,
  middle?: string,
  last: string,
};
function computeFullName(person: Person) { /*...*/ }

Property .middle is optional and can be omitted. To TypeScript, mistyping its name looks like omitting it and providing an excess property. However, it still catches the typo because excess properties are not allowed in this case:

// @ts-expect-error: Object literal may only specify known properties, but
// 'mdidle' does not exist in type 'Person'. Did you mean to write
// 'middle'?
computeFullName({first: 'Jane', mdidle: 'Cecily', last: 'Doe'});

18.3.2 Why are excess properties allowed if an object comes from somewhere else?

The idea is that if an object comes from somewhere else, we can assume that it has already been vetted and will not have any typos. Then we can afford to be less careful.

If typos are not an issue, our goal should be maximizing flexibility. Consider the following function:

type HasYear = {
  year: number,
};

function getAge(obj: HasYear) {
  const yearNow = new Date().getFullYear();
  return yearNow - obj.year;
}

Without allowing excess properties for values that are passed to getAge(), the usefulness of this function would be quite limited.

18.3.3 Empty object literal types allow excess properties

If an object literal type is empty, excess properties are always allowed:

type Empty = {};
type OneProp = {
  myProp: number,
};

// @ts-expect-error: Object literal may only specify known properties, and
// 'anotherProp' does not exist in type 'OneProp'.
const a: OneProp = { myProp: 1, anotherProp: 2 };
const b: Empty = { myProp: 1, anotherProp: 2 }; // OK

18.3.4 Matching only objects without properties

If we want to enforce that an object has no properties, we can use the following trick (credit: Geoff Goodman):

type WithoutProperties = {
  [key: string]: never,
};

// @ts-expect-error: Type 'number' is not assignable to type 'never'.
const a: WithoutProperties = { prop: 1 };
const b: WithoutProperties = {}; // OK

18.3.5 Allowing excess properties in object literals

What if we want to allow excess properties in object literals? As an example, consider type Point and function computeDistance1():

type Point = {
  x: number,
  y: number,
};

function computeDistance1(point: Point) { /*...*/ }

// @ts-expect-error: Object literal may only specify known properties, and
// 'z' does not exist in type 'Point'.
computeDistance1({ x: 1, y: 2, z: 3 });

One option is to assign the object literal to an intermediate variable:

const obj = { x: 1, y: 2, z: 3 };
computeDistance1(obj);

A second option is to use a type assertion:

computeDistance1({ x: 1, y: 2, z: 3 } as Point); // OK

A third option is to rewrite computeDistance1() so that it uses a type parameter:

function computeDistance2<P extends Point>(point: P) { /*...*/ }
computeDistance2({ x: 1, y: 2, z: 3 }); // OK

A fourth option is to extend Point so that it allows excess properties:

type PointEtc = Point & {
  [key: string]: any;
};
function computeDistance3(point: PointEtc) { /*...*/ }

computeDistance3({ x: 1, y: 2, z: 3 }); // OK

We used an intersection type (& operator) to define PointEtc. For more information, see “Intersections of object types” (§20.1).

We’ll continue with two examples where TypeScript not allowing excess properties, is a problem.

18.3.5.1 Allowing excess properties: example Incrementor factory

In this example, we implement a factory for objects of type Incrementor and would like to return a subtype, but TypeScript doesn’t allow the extra property .counter:

type Incrementor = {
  inc(): number,
};
function createIncrementor(): Incrementor {
  return {
    // @ts-expect-error: Object literal may only specify known properties, and
    // 'counter' does not exist in type 'Incrementor'.
    counter: 0,
    inc() {
      // @ts-expect-error: Property 'counter' does not exist on type
      // 'Incrementor'.
      return this.counter++;
    },
  };
}

Alas, even with a type assertion, there is still one type error:

function createIncrementor2(): Incrementor {
  return {
    counter: 0,
    inc() {
      // @ts-expect-error: Property 'counter' does not exist on type
      // 'Incrementor'.
      return this.counter++;
    },
  } as Incrementor;
}

What does work is as any but then the type of the returned object is any and, e.g. inside .inc(), TypeScript doesn’t check if properties of this really exist.

A proper solution is to add an index signature to Incrementor. Or – especially if that is not possible – to introduce an intermediate variable:

function createIncrementor3(): Incrementor {
  const incrementor = {
    counter: 0,
    inc() {
      return this.counter++;
    },
  };
  return incrementor;
}
18.3.5.2 Allowing excess properties: example .dateStr

The following comparison function can be used to sort objects that have the property .dateStr:

function compareDateStrings(
  a: {dateStr: string}, b: {dateStr: string}) {
    if (a.dateStr < b.dateStr) {
      return +1;
    } else if (a.dateStr > b.dateStr) {
      return -1;
    } else {
      return 0;
    }
  }

For example in unit tests, we may want to invoke this function directly with object literals. TypeScript doesn’t let us do this and we need to use one of the workarounds.

18.4 Object types and inherited properties

18.4.1 TypeScript doesn’t distinguish own and inherited properties

TypeScript doesn’t distinguish own and inherited properties. They are all simply considered to be properties.

type MyType = {
  toString(): string, // inherited property
  prop: number, // own property
};
const obj: MyType = { // OK
  prop: 123,
};

obj inherits .toString() from Object.prototype.

The downside of this approach is that some phenomena in JavaScript can’t be described via TypeScript’s type system. The upside is that the type system is simpler.

18.4.2 Object literal types describe instances of Object

All object literal types describe objects that are instances of Object and inherit the properties of Object.prototype. In the following example, the parameter x of type {} is compatible with the return type Object:

function f1(x: {}): Object {
  return x;
}

Similarly, {} has a method .toString():

function f2(x: {}): { toString(): string } {
  return x;
}

18.5 Interfaces vs. object literal types

For historical reasons, object types can be defined in two ways:

// Object literal type
type ObjType1 = {
  a: boolean,
  b: number,
  c: string,
};

// Interface
interface ObjType2 {
  a: boolean;
  b: number;
  c: string;
}

Both ways of defining an object type are more or less equivalent now. We’ll dive into the (minor) differences next.

18.5.1 Object literal types can be inlined

Object literal types can be inlined, while interfaces can’t be:

// The object literal type is inlined
// (mentioned inside the parameter definition)
function f1(x: {prop: number}) {}

// We can’t mention the interface inside the parameter definition.
// We can only define it externally and refer to it.
function f2(x: ObjectInterface) {} 
interface ObjectInterface {
  prop: number;
}

18.5.2 Interfaces with the same name are merged

Type aliases with duplicate names are illegal:

// @ts-expect-error: Duplicate identifier 'PersonAlias'.
type PersonAlias = {first: string};
// @ts-expect-error: Duplicate identifier 'PersonAlias'.
type PersonAlias = {last: string};

Conversely, interfaces with duplicate names are merged:

interface PersonInterface {
  first: string;
}
interface PersonInterface {
  last: string;
}
const jane: PersonInterface = {
  first: 'Jane',
  last: 'Doe',
};

This is called declaration merging and can be used to combine types from multiple sources – e.g., as long as Array.fromAsync() is a new method, it is not part of the core library declaration file, but provided via lib.esnext.array.d.ts – which adds it as an increment to ArrayConstructor (the type of Array as a class value):

interface ArrayConstructor {
  fromAsync<T>(···): Promise<T[]>;
}

18.5.3 Mapped types look like object literal types

A mapped type (line A) looks like an object literal type:

type Point = {
  x: number,
  y: number,
};

type PointCopy1 = {
  [Key in keyof Point]: Point[Key] // (A)
};

As an option, we can end line A with a semicolon. Alas, a comma is not allowed.

For more information on this topic, see “Mapped types {[K in U]: X}” (§36).

18.5.4 Only interfaces support polymorphic this types

Polymorphic this types can only be used in interfaces:

interface AddsStrings {
  add(str: string): this;
};

class StringBuilder implements AddsStrings {
  result = '';
  add(str: string): this {
    this.result += str;
    return this;
  }
}

18.5.5 Only interfaces support extends – but type intersection (&) is similar

An interface B can extend another interface A and is then interpreted as an increment of A:

interface A {
  propA: number;
}
interface B extends A {
  propB: number;
}
type _ = Assert<Equal<
  B,
  {
    propA: number,
    propB: number,
  }
>>;

Object literal types don’t support extend but an intersection type & has a similar effect:

type A = {
  propA: number,
};
type B = {
  propB: number,
} & A;
type _ = Assert<Equal<
  B,
  {
    propA: number,
    propB: number,
  }
>>;

Intersections of object types are described in more detail in another chapter. Here, we’ll explore how exactly they differ from extends.

18.5.5.1 Conflicts

If there is a conflict between an extending interface and an extended interface then that’s an error:

interface A {
  prop: string;
}
// @ts-expect-error: Interface 'B' incorrectly extends interface 'A'.
// Types of property 'prop' are incompatible.
interface B extends A {
  prop: number;
}

In contrast, intersection types don’t complain about conflicts, but they may result in never in some locations:

type A = {
  prop: string,
};
type B = {
  prop: number,
} & A;
type _ = Assert<Equal<
  B,
  {
    prop: number & string, // never
  }
>>;
18.5.5.2 Only interfaces support overriding

Overriding a method means replacing a method in a supertype with a compatible method – roughly:

interface A {
  m(x: string): Object;
}
interface B extends A {
  m(x: string | number): RegExp;
}

type _ = Assert<Equal<
  B,
  {
    m(x: string | number): RegExp,
  }
>>;

function f(x: B) {
  assertType<RegExp>(x.m('abc'));
}

We can see that the overriding method “wins” and completely replaces the overridden method in B. In contrast, both methods exist in parallel in an intersection type:

type A = {
  m(x: string): Object,
};
type B = {
  m(x: string | number): RegExp,
};

type _ = [
  Assert<Equal<
    A & B,
    {
      m: ((x: string) => Object) & ((x: string | number) => RegExp),
    }
  >>,
  Assert<Equal<
    B & A,
    {
      m: ((x: string | number) => RegExp) & ((x: string) => Object),
    }
  >>,
];

function f1(x: A & B) {
  assertType<Object>(x.m('abc')); // (A)
}
function f2(x: B & A) {
  assertType<RegExp>(x.m('abc')); // (B)
}

When it comes to the return type (line A and line B), the earlier member of the intersection wins. That’s why B & A (B1) is more similar to B extends A, even though A & B (B2) looks nicer:

type B1 = {
  prop: number,
} & A;
type B2 = A & {
  prop: number,
};
18.5.5.3 extends or & – which one to use?

Which one to use depends on the context. If inheritance is involved then an interface and extends is usually the better choice due to their support of overriding.

Icon “external”Source of this section

18.6 Forbidding properties via never

Given that no other type is assignable to never, we can use it to forbid properties.

18.6.1 Forbidding properties with string keys

The type EmptyObject forbids string keys:

type EmptyObject = Record<string, never>;

// @ts-expect-error: Type 'number' is not assignable to type 'never'.
const obj1: EmptyObject = { prop: 123 };
const obj2: EmptyObject = {}; // OK

In contrast, the type {} is assignable from all objects and not a type for empty objects:

const obj3: {} = { prop: 123 };

18.6.2 Forbidding index properties (with number keys)

The type NoIndices forbids number keys but allows the string key 'prop':

type NoIndices = Record<number, never> & { prop?: boolean };

//===== Objects =====
const obj1: NoIndices = {}; // OK
const obj2: NoIndices = { prop: true }; // OK
// @ts-expect-error: Type 'string' is not assignable to type 'never'.
const obj3: NoIndices = { 0: 'a' }; // OK

//===== Arrays =====
const arr1: NoIndices = []; // OK
// @ts-expect-error: Type 'string' is not assignable to type 'never'.
const arr2: NoIndices = ['a'];

18.7 Index signatures: objects as dictionaries

So far, we have only used types for fixed-layout objects. How do we express the fact that an object is to be used as a dictionary? For example: What should TranslationDict be in the following code fragment?

function translate(dict: TranslationDict, english: string): string {
  const translation = dict[english];
  if (translation === undefined) {
    throw new Error();
  }
  return translation;
}

One option is to use an index signature (line A) to express that TranslationDict is for objects that map string keys to string values (another option is Record – which we’ll get to later):

type TranslationDict = {
  [key: string]: string, // (A)
};
const dict = {
  'yes': 'sí',
  'no': 'no',
  'maybe': 'tal vez',
};
assert.equal(
  translate(dict, 'maybe'),
  'tal vez');

The name key doesn’t matter – it can be any identifier and is ignored (but can’t be omitted).

18.7.1 Typing index signature keys

An index signature represents an infinite set of properties; only the following types are allowed:

Specifically not allowed are:

If you need more power then consider using a mapped types.

These are examples of index signatures:

type IndexSignature1 = {
  [key: string]: boolean,
};
// Template string literal with infinite primitive type
type IndexSignature2 = {
  [key: `${bigint}`]: string,
};
// Union of previous types
type IndexSignature3 = {
  [key: string | `${bigint}`]: string,
};

18.7.2 String keys vs. number keys

Just like in plain JavaScript, TypeScript’s number property keys are a subset of the string property keys (see “Exploring JavaScript”). Accordingly, if we have both a string index signature and a number index signature, the property type of the former must be a supertype of the latter. The following example works because Object is a supertype of RegExp (RegExp is assignable to Object):

type StringAndNumberKeys = {
  [key: string]: Object,
  [key: number]: RegExp,
};

The following code demonstrates the effects of using strings and numbers as property keys:

function f(x: StringAndNumberKeys) {
  return {
    str: x['abc'],
    num: x[123],
  };
}
assertType<
  (x: StringAndNumberKeys) => {
    str: Object | undefined,
    num: RegExp | undefined,
  }
>(f);

18.7.3 Index signatures vs. property signatures and method signatures

If there are both an index signature and property and/or method signatures in an object literal type, then the type of the index property value must also be a supertype of the type of the property value and/or method.

type T1 = {
  [key: string]: boolean,

  // @ts-expect-error: Property 'myProp' of type 'number' is not assignable
  // to 'string' index type 'boolean'.
  myProp: number,
  
  // @ts-expect-error: Property 'myMethod' of type '() => string' is not
  // assignable to 'string' index type 'boolean'.
  myMethod(): string,
};

In contrast, the following two object literal types produce no errors:

type T2 = {
  [key: string]: number,
  myProp: number,
};

type T3 = {
  [key: string]: () => string,
  myMethod(): string,
}

18.8 Record<K, V> for dictionary objects

The built-in generic utility type Record<K, V> is for dictionary objects whose keys are of type K and whose values are of type V:

const dict: Record<string, number> = {
  one: 1,
  two: 2,
  three: 3,
};

If you are curious how Record is defined: Record is a mapped type” (§36.6). This knowledge can help with remembering how it handles finite and infinite key types.

Record supports unions of literal types as key types; index signatures don’t. More on that next.

18.8.1 Index signatures don’t allow key unions

The key type of an index signature must be infinite:

type Key = 'A' | 'B' | 'C';

// @ts-expect-error: An index signature parameter type cannot be a literal
// type or generic type. Consider using a mapped object type instead.
const dict: {[key: Key]: true} = {
  A: true,
  C: true,
};

18.8.2 Record enforces exhaustiveness for key unions

Record enforces exhaustiveness if its key type is a union of literal types:

type T = 'A' | 'B' | 'C';

// @ts-expect-error: Property 'C' is missing in type '{ A: true; B: true; }'
// but required in type 'Record<T, true>'.
const nonExhaustiveKeys: Record<T, true> = {
  A: true,
  B: true,
};

const exhaustiveKeys: Record<T, true> = {
  A: true,
  B: true,
  C: true,
};

Wrong keys also produce errors:

const wrongKey: Record<T, true> = {
  A: true,
  B: true,
  // @ts-expect-error: Object literal may only specify known properties,
  // and 'D' does not exist in type 'Record<T, true>'.
  D: true,
};

18.8.3 Record: preventing exhaustiveness checks for key unions

If we want to prevent exhaustiveness checks for keys whose type is a union then we can use the utility type Partial (which makes all properties optional). Then we can omit some properties, but wrong keys still produce errors:

type T = 'A' | 'B' | 'C';
const nonExhaustiveKeys: Partial<Record<T, true>> = {
  A: true,
};
const wrongKey: Partial<Record<T, true>> = {
  // @ts-expect-error: Object literal may only specify known properties,
  // and 'D' does not exist in type 'Partial<Record<T, true>>'.
  D: true,
};

18.9 object vs Object vs. {}

These are three similar general types for objects:

Icon “tip”These types are not used that often

Given that we can’t access any properties if we use these types, they are not used that often. If a value does have that type, we usually narrow its type via a type guard before doing anything with it.

18.9.1 Plain JavaScript: objects vs. instances of Object

In plain JavaScript, there is an important distinction.

On one hand, most objects are instances of Object.

> const obj1 = {};
> obj1 instanceof Object
true

That means:

On the other hand, we can also create objects that don’t have Object.prototype in their prototype chains. For example, the following object does not have any prototype at all:

> const obj2 = Object.create(null);
> Object.getPrototypeOf(obj2)
null

obj2 is an object that is not an instance of class Object:

> typeof obj2
'object'
> obj2 instanceof Object
false

18.9.2 Object (uppercase “O”) in TypeScript: instances of class Object

Recall that each class C creates two entities:

Similarly, there are two object types for class Object:

These are the types:

interface Object { // (A)
  constructor: Function;
  toString(): string;
  toLocaleString(): string;
  /** Returns the primitive value of the specified object. */
  valueOf(): Object; // (B)
  hasOwnProperty(v: PropertyKey): boolean;
  isPrototypeOf(v: Object): boolean;
  propertyIsEnumerable(v: PropertyKey): boolean;
}

interface ObjectConstructor {
  /** Invocation via `new` */
  new(value?: any): Object;
  /** Invocation via function calls */
  (value?: any): any;

  readonly prototype: Object; // (C)

  getPrototypeOf(o: any): any;
  // ···
}
declare var Object: ObjectConstructor; // (D)

Observations:

18.9.3 Type {} basically means: not nullish

{} accepts all values other than undefined and null:

const v1: {} = 123;
const v2: {} = 123;
const v3: {} = {};
const v4: {} = { prop: true };

// @ts-expect-error: Type 'undefined' is not assignable to type '{}'.
const v5: {} = undefined;
// @ts-expect-error: Type 'null' is not assignable to type '{}'.
const v6: {} = null;

The helper type NonNullable uses {}:

/**
 * Exclude null and undefined from T
 */
type NonNullable<T> = T & {};

type _ = [
  Assert<Equal<
    NonNullable<undefined | string>,
    string
  >>,
  Assert<Equal<
    NonNullable<null | string>,
    string
  >>,
  Assert<Equal<
    NonNullable<string>,
    string
  >>,
];

The result of NonNullable<T> is a type that is the intersection of T and all non-nullish values.

18.9.4 Inferred types for various objects

These are the types that TypeScript infers for objects that are created via various means:

const obj1 = new Object();
assertType<Object>(obj1);

const obj2 = Object.create(null);
assertType<any>(obj2);

const obj3 = {};
assertType<{}>(obj3);

const obj4 = {prop: 123};
assertType<{prop: number}>(obj4);

const obj5 = Reflect.getPrototypeOf({});
assertType<object | null>(obj5);

In principle, the return type of Object.create() could (and probably should) be object or a computed type. However, for historic reasons, it is any. That allows us to add and change properties of the result.

18.10 Summary: object vs Object vs. {} vs. Record

The following table compares four types for objects:

objectObject{}Record
Accepts undefined or null
Accepts primitive values
Has .toString()N/A
Values can conflict with ObjectN/A

The last two table rows don’t really make sense for Record – which is why there is an “N/A” in its cells.

Accepts undefined or null:

type _ = [
  Assert<Not<Assignable<
    object, undefined
  >>>,
  Assert<Not<Assignable<
    Object, undefined
  >>>,
  Assert<Not<Assignable<
    {}, undefined
  >>>,
  Assert<Not<Assignable<
    Record<keyof any, any>, undefined
  >>>,
];

Accepts primitive values:

type _ = [
  Assert<Not<Assignable<
    object, 123
  >>>,
  Assert<Assignable<
    Object, 123
  >>,
  Assert<Assignable<
    {}, 123
  >>,
  Assert<Not<Assignable<
    Record<keyof any, any>, 123
  >>>,
];

Has .toString():

type _ = [
  Assert<Assignable<
    { toString(): string }, object
  >>,
  Assert<Assignable<
    { toString(): string }, Object
  >>,
  Assert<Assignable<
    { toString(): string }, {}
  >>,
];

Values can conflict with Object:

type _ = [
  Assert<Assignable<
    object, { toString(): number }
  >>,
  Assert<Not<Assignable<
    Object, { toString(): number }
  >>>,
  Assert<Assignable<
    {}, { toString(): number }
  >>,
];

18.11 Sources of this chapter