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

Table of contents:

Roles played by objects #

In JavaScript, objects can play two roles (always at least one of them, sometimes mixtures):

Records: A fixed amount of properties that are known at development time. Each property can have a different type.

Dictionaries: An arbitrary amount of properties whose names are not known at development time. All property keys (strings and/or symbols) have the same type, as do the property values.

First and foremost, we will explore objects as records. We will briefly encounter objects as dictionaries later in this post.

Types for objects #

There are two different general types for objects:

Object with an uppercase “O” is the type of all instances of class Object : let obj1: Object ;

with an uppercase “O” is the type of all instances of class : object with a lowercase “o” is the type of all non-primitive values: let obj2: object;

Objects can also be described via their properties:

let obj3: {prop: boolean }; interface ObjectType { prop: boolean ; } let obj4: ObjectType;

In the next sections, we’ll examine all these ways of typing objects in more detail.

Object vs. object in TypeScript #

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:

Object.prototype is in their prototype chains: > Object.prototype.isPrototypeOf(obj1) true

They inherit its properties. > obj1.toString === Object.prototype.toString true

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

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

In TypeScript, Object is the type of all instances of class Object . It is defined by two interfaces:

Interface Object defines the properties of Object.prototype .

defines the properties of . Interface ObjectConstructor defines the properties of class Object (i.e., the object pointed to by that global variable).

interface Object { constructor : Function; toString( ): string; toLocaleString( ): string; valueOf( ): Object; hasOwnProperty( v: PropertyKey ): boolean; isPrototypeOf( v: Object ): boolean; propertyIsEnumerable( v: PropertyKey ): boolean; } interface ObjectConstructor { new (value?: any ): Object ; (value?: any ): any ; readonly prototype: Object ; getPrototypeOf(o: any ): any ; } declare var Object : ObjectConstructor;

All instances of Object inherit the properties of interface Object . We can see that if we create a function that returns its parameter: If an instance of Object comes in, it always satisfies the return type – which requires it to have a method .toString() .

function f ( x: Object ): { toString(): string } { return x; }

object (lowercase “o”) in TypeScript: non-primitive values #

In TypeScript, object is the type of all non-primitive values (primitive values are undefined , null , booleans, numbers, bigints, strings). With this type, we can’t access any properties of a value.

Object vs. object : primitive values #

Interestingly, type Object includes primitive values:

function func1 ( x: Object ) { } func1( 'abc' );

Why? The properties of Object.prototype can also be accessed via primitive values:

> 'abc'.hasOwnProperty === Object.prototype.hasOwnProperty true

Conversely, object does not include primitive values:

function func2 ( x: object ) { } func2( 'abc' );

Object vs. object : incompatible property types #

With type Object , TypeScript complains if an object has a property whose type conflicts with the corresponding property in interface Object :

const obj1: Object = { toString() { return 123 } };

With type object , TypeScript does not complain (because object has no properties and there can’t be any conflicts):

const obj2: object = { toString() { return 123 } };

Object type literals and interfaces #

TypeScript has two ways of defining object types that are very similar:

type ObjType1 = { a: boolean , b: number ; c: string , }; interface ObjType2 { a: boolean , b: number ; c: string , }

We can use either semicolons or commas as separators. Trailing separators are allowed and optional.

Differences between object type literals and interfaces #

In this section, we take a look at the most important differences between object type literals and interfaces.

Source of this section: GitHub issue “TypeScript: types vs. interfaces” by Johannes Ewald

Object type literals can be inlined, while interfaces can’t be:

function f1 ( x: {prop: number } ) {} function f2 ( x: ObjectInterface ) {} interface ObjectInterface { prop: number ; }

Duplicate names #

Type aliases with duplicate names are illegal:

type PersonAlias = {first: string }; 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' , };

Mapped types #

For Mapped types (line A), we need to use object type literals:

interface Point { x: number ; y: number ; } type PointCopy1 = { [Key in keyof Point]: Point[Key]; };

Polymorphic this types #

Only possible in interfaces:

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

From now on, “interface” means “interface or object type literal” (unless stated otherwise).

Nominal type systems vs. structural type systems #

One of the responsibilities of a static type system is to determine if two static types are compatible:

The static type U of an actual parameter (provided, e.g., via a function call)

of an actual parameter (provided, e.g., via a function call) The static type T of the corresponding formal parameter (specified as part of a function definition)

This often means checking if U is a subtype of T . Two approaches for this check are (roughly):

In a nominal or nominative type system, two static types are equal if they have the same identity (“name”). One type is a subtype of another if their subtype relationship was declared explicitly. Languages with nominal typing are C++, Java, C#, Swift, and Rust.

In a structural type system, two static types are equal if they have the same structure (if their parts have the same names and the same types). One type U is a subtype of another type T if U has all parts of T (and possibly others) and each part of U has a subtype of the corresponding part of T . Languages with structural typing are OCaml/ReasonML, Haskell, and TypeScript.



The following code produces a type error (line A) in nominal type systems, but is legal in TypeScript’s structural type system because class A and class B have the same structure:

class A { name = 'A' ; } class B { name = 'B' ; } const someVariable: A = new B();

TypeScript’s interfaces also work structurally – they don’t have to be implemented in order to “match”:

interface Point { x: number ; y: number ; } const point: Point = {x: 1 , y: 2 };

Members of interfaces and object type literals #

interface ExampleInterface { myProperty: boolean ; myMethod(x: string ): void ; [prop: string ]: any ; (x: number ): string ; new (x: string ): ExampleInstance; } interface ExampleInstance {}

Members of interfaces and object type literals can be:

Property signatures define properties: myProperty: boolean ;

Method signatures define methods: myMethod(x: string ): void ; Note that the names of parameters (in this case: x ) help with documenting how things work, but have no other purpose.

Index signatures help when interfaces describe Arrays or objects that are used as dictionaries. [prop: string ]: any ; Note: The property key name prop is only there for documentation purposes. I often use key or k .

Call signatures enable interfaces to describe functions: (x: number ): string ;

Constructor signatures enable interfaces to describe classes and constructor functions: new (x: string ): ExampleInstance;

Property signatures and method signatures should be self-explanatory. Call and constructor signatures are beyond the scope of this blog post. We’ll take a closer look at index signatures next.

Index signatures: objects as dicts #

So far, we have only used interfaces for objects-as-records with fixed keys. 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 { return dict[english]; }

We use an index signature (line A) to express that TranslationDict is for objects that map string keys to string values:

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

Typing index signature keys #

Index signature keys must be either string or number :

Symbols are not allowed.

any is not allowed.

is not allowed. Type unions (e.g. string|type ) are not allowed. However, multiple index signatures can be used per interface.

String keys vs. number keys #

Just like in plain JavaScript, TypeScript’s number property keys are a subset of the string property keys (see “JavaScript for impatient programmers”). 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 :

interface StringAndNumberKeys { [key: string ]: Object ; [key: number ]: RegExp ; } function f ( x: StringAndNumberKeys ) { return { str: x[ 'abc' ], num: x[ 123 ] }; }

Index signatures vs. property and method signatures #

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

interface I1 { [key: string ]: boolean ; myProp: number ; myMethod(): string ; }

In contrast, the following two interfaces produce no errors:

interface I2 { [key: string ]: number ; myProp: number ; } interface I3 { [key: string ]: () => string ; myMethod(): string ; }

Interfaces describe instances of Object #

All interfaces 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 result type Object :

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

Similarly, {} is understood to have a method .toString() :

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

Excess property checks #

As an example, consider the following interface:

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

There are two ways (among others) in which this interface could be interpreted:

Closed interpretation: It could describe all objects that have exactly the properties .x and .y with the specified types. On other words: Those objects must not have excess properties (more than the required properties).

and with the specified types. On other words: Those objects must not have excess properties (more than the required properties). Open interpretation: It could describe all objects that have at least the properties .x and .y . In other words: Excess properties are allowed.

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);

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

computeDistance({ x: 1 , y: 2 , z: 3 }); computeDistance({x: 1 , y: 2 });

Why are excess properties forbidden in object literals? #

Why the restriction? The open interpretation that allows excess properties is reasonably safe when the data comes from somewhere else. However, if we create the data ourselves, then we profit from the extra protection against typos that the closed interpretation gives us – for example:

interface Person { first: string ; middle?: string ; last: string ; } function computeFullName ( person: Person ) { }

Property .middle is optional and can be omitted (we’ll examine optional properties in more detail later). If we mistype its name in an object literal, TypeScript will assume that we created an excess property and left out .middle . Thankfully, we get a warning because excess properties are not allowed in object literals:

computeFullName({first: 'Jane' , mdidle: 'Cecily' , last: 'Doe' });

If an object with the same typo came from somewhere else, it would be accepted.

Empty interfaces allow excess properties #

If an interface is empty (or the object type literal {} is used), excess properties are always allowed:

interface Empty { } interface OneProp { myProp: number ; } const a: OneProp = { myProp: 1 , anotherProp: 2 }; const b: Empty = {myProp: 1 , anotherProp: 2 };

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

interface WithoutProperties { [key: string ]: never; } const a: WithoutProperties = { prop: 1 }; const b: WithoutProperties = {};

Allowing excess properties in object literals #

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

interface Point { x: number ; y: number ; } function computeDistance1 ( point: 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);

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 });

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

interface PointEtc extends Point { [key: string ]: any ; } function computeDistance3 ( point: PointEtc ) { } computeDistance3({ x: 1 , y: 2 , z: 3 });

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

Excess properties: example 1 #

In this example, we’d like to implement an Incrementor , but TypeScript doesn’t allow the extra property .counter :

interface Incrementor { inc(): void } function createIncrementor ( start = 0 ): Incrementor { return { counter: start, inc() { this .counter++; }, }; }

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

function createIncrementor2 ( start = 0 ): Incrementor { return { counter: start, inc() { this .counter++; }, } as Incrementor; }

We can either add an index signature to interface Incrementor . Or – especially if that is not possible – we can introduce an intermediate variable:

function createIncrementor3 ( start = 0 ): Incrementor { const incrementor = { counter: start, inc() { this .counter++; }, }; return incrementor; }

Excess properties: example 2 #

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 work-arounds.

Type inference #

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

const obj1 = new Object (); const obj2 = Object .create( null ); const obj3 = {}; const obj4 = {prop: 123 }; const obj5 = Reflect.getPrototypeOf({});

In principle, the return type of Object.create() could be object . I assume that it is any to be backward compatible with old code.

Other features of interfaces #

Optional properties #

If we put a question mark ( ? ) after the name of a property, that property is declared to be optional. For example, in the following example, property .middle is optional:

interface Name { first: string ; middle?: string ; last: string ; }

That means that it’s OK to omit it (line A):

const john = {first: 'Doe' , last: 'Doe' }; const jane = {first: 'Jane' , middle: 'Cecily' , last: 'Doe' };

What is the difference between .prop1 and .prop2 ?

interface Interf { prop1?: string ; prop2: undefined | string ; }

An optional property can do everything that undefined|string can. We can even use the value undefined for the former:

const obj1: Interf = { prop1: undefined , prop2: undefined };

However, only .prop1 can be omitted:

const obj2: Interf = { prop2: undefined }; const obj3: Interf = { };

Types such as undefined|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.

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

interface MyInterface { readonly prop: number ; }

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

const obj: MyInterface = { prop: 1 , }; console .log(obj.prop); obj.prop = 2 ;

JavaScript’s prototype chains and TypeScript’s types #

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

interface MyInterface { toString(): string ; prop: number ; } const obj: MyInterface = { prop: 123 , };

The downside of this approach is that there are some JavaScript phenomena that can’t be typed statically. Its upside is that the type system is simpler.

Sources of this blog post #