Object
vs. object
in TypeScript
Object
In this chapter, we will explore how objects and properties are typed statically in TypeScript.
In JavaScript, objects can play two roles (always at least one of them, sometimes mixtures):
Records have a fixed amount of properties that are known at development time. Each property can have a different type.
Dictionaries have an arbitrary number of properties whose names are not known at development time. All property keys (strings and/or symbols) have the same type, as have property values.
First and foremost, we will explore objects as records. We will briefly encounter objects as dictionaries later in this chapter.
There are two different general types for objects:
Object
with an uppercase “O” is the type of all instances of class Object
:
: Object; let obj1
object
with a lowercase “o” is the type of all non-primitive values:
: object; let obj2
Objects can also be typed via their properties:
// Object type literal
: {prop: boolean};
let obj3
// Interface
interface ObjectType {: boolean;
prop
}: ObjectType; let obj4
In the next sections, we’ll examine all these ways of typing objects in more detail.
Object
vs. object
in TypeScriptObject
In plain JavaScript, there is an important distinction.
On one hand, most objects are instances of Object
.
> const obj1 = {};
> obj1 instanceof Objecttrue
That means:
Object.prototype
is in their prototype chains:
> Object.prototype.isPrototypeOf(obj1)true
They inherit its properties.
> obj1.toString === Object.prototype.toStringtrue
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 Objectfalse
Object
(uppercase “O”) in TypeScript: instances of class Object
Recall that each class C
creates two entities:
C
.C
that describes instances of the constructor function.Similarly, TypeScript has two built-in interfaces:
Interface Object
specifies the properties of instances of Object
, including the properties inherited from Object.prototype
.
Interface ObjectConstructor
specifies the properties of class Object
.
These are the interfaces:
// (A)
interface Object { : Function;
constructortoString(): string;
toLocaleString(): string;
valueOf(): Object;
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 */
?: any): any;
(value
readonly prototype: Object; // (B)
getPrototypeOf(o: any): any;
// ···
}declare var Object: ObjectConstructor; // (C)
Observations:
Object
(line C) and a type whose name is Object
(line A).Object
have no own properties, therefore Object.prototype
also matches Object
(line B).object
(lowercase “o”) in TypeScript: non-primitive valuesIn 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 valuesInterestingly, type Object
also matches primitive values:
function func1(x: Object) { }
func1('abc'); // OK
Why is that? Primitive values have all the properties required by Object
because they inherit Object.prototype
:
> 'abc'.hasOwnProperty === Object.prototype.hasOwnPropertytrue
Conversely, object
does not match primitive values:
function func2(x: object) { }
// @ts-expect-error: Argument of type '"abc"' is not assignable to
// parameter of type 'object'. (2345)
func2('abc');
Object
vs. object
: incompatible property typesWith type Object
, TypeScript complains if an object has a property whose type conflicts with the corresponding property in interface Object
:
// @ts-expect-error: Type '() => number' is not assignable to
// type '() => string'.
// Type 'number' is not assignable to type 'string'. (2322)
: Object = { toString() { return 123 } }; const obj1
With type object
, TypeScript does not complain (because object
does not specify any properties and there can’t be any conflicts):
: object = { toString() { return 123 } }; const obj2
TypeScript has two ways of defining object types that are very similar:
// Object type literal
type ObjType1 = {
: boolean,
a: number;
b: string,
c;
}
// Interface
interface ObjType2 {: boolean,
a: number;
b: string,
c }
We can use either semicolons or commas as separators. Trailing separators are allowed and optional.
In this section, we take a look at the most important differences between object type literals and interfaces.
Object type literals can be inlined, while interfaces can’t be:
// Inlined object type literal:
function f1(x: {prop: number}) {}
// Referenced interface:
function f2(x: ObjectInterface) {}
interface ObjectInterface {: number;
prop }
Type aliases with duplicate names are illegal:
// @ts-expect-error: Duplicate identifier 'PersonAlias'. (2300)
type PersonAlias = {first: string};
// @ts-expect-error: Duplicate identifier 'PersonAlias'. (2300)
type PersonAlias = {last: string};
Conversely, interfaces with duplicate names are merged:
interface PersonInterface {: string;
first
}
interface PersonInterface {: string;
last
}: PersonInterface = {
const jane: 'Jane',
first: 'Doe',
last; }
For Mapped types (line A), we need to use object type literals:
interface Point {: number;
x: number;
y
}
type PointCopy1 = {
keyof Point]: Point[Key]; // (A)
[Key in ;
}
// Syntax error:
// interface PointCopy2 {
// [Key in keyof Point]: Point[Key];
// };
More information on mapped types
Mapped types are beyond the current scope of this book. For more information, see the TypeScript Handbook.
this
typesPolymorphic this
types can only be used in interfaces:
interface AddsStrings {add(str: string): this;
;
}
class StringBuilder implements AddsStrings {= '';
result add(str: string) {
.result += str;
this;
return this
} }
Source of this section
From now on, “interface” means “interface or object type literal” (unless stated otherwise).
Interfaces work structurally – they don’t have to be implemented in order to match:
interface Point {: number;
x: number;
y
}: Point = {x: 1, y: 2}; // OK const point
For more information on this topic, see [content not included].
The constructs inside the bodies of interfaces and object type literals are called their members. These are the most common members:
interface ExampleInterface {// Property signature
: boolean;
myProperty
// Method signature
myMethod(str: string): number;
// Index signature
: string]: any;
[key
// Call signature
: number): string;
(num
// Construct signature
new(str: string): ExampleInstance;
} interface ExampleInstance {}
Let’s look at these members in more detail:
Property signatures define properties:
: boolean; myProperty
Method signatures define methods:
myMethod(str: string): number;
Note: The names of parameters (in this case: str
) help with documenting how things work but have no other purpose.
Index signatures are needed to describe Arrays or objects that are used as dictionaries.
: string]: any; [key
Note: The name key
is only there for documentation purposes.
Call signatures enable interfaces to describe functions:
: number): string; (num
Construct signatures enable interfaces to describe classes and constructor functions:
new(str: string): ExampleInstance;
Property signatures should be self-explanatory. Call signatures and construct signatures are described later in this book. We’ll take a closer look at method signatures and index signatures next.
As far as TypeScript’s type system is concerned, method definitions and properties whose values are functions, are equivalent:
interface HasMethodDef {simpleMethod(flag: boolean): void;
}
interface HasFuncProp {: (flag: boolean) => void;
simpleMethod
}
: HasMethodDef = {
const objWithMethodsimpleMethod(flag: boolean): void {},
;
}: HasFuncProp = objWithMethod;
const objWithMethod2
: HasMethodDef = {
const objWithOrdinaryFunction: function (flag: boolean): void {},
simpleMethod;
}: HasFuncProp = objWithOrdinaryFunction;
const objWithOrdinaryFunction2
: HasMethodDef = {
const objWithArrowFunction: (flag: boolean): void => {},
simpleMethod;
}: HasFuncProp = objWithArrowFunction; const objWithArrowFunction2
My recommendation is to use whichever syntax best expresses how a property should be set up.
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 {:string]: string; // (A)
[key
}= {
const dict 'yes': 'sí',
'no': 'no',
'maybe': 'tal vez',
;
}.equal(
asserttranslate(dict, 'maybe'),
'tal vez');
Index signature keys must be either string
or number
:
any
is not allowed.string|number
) are not allowed. However, multiple index signatures can be used per interface.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 {: string]: Object;
[key: number]: RegExp;
[key
}
// %inferred-type: (x: StringAndNumberKeys) =>
// { str: Object; num: RegExp; }
function f(x: StringAndNumberKeys) {
: x['abc'], num: x[123] };
return { str }
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 {: string]: boolean;
[key
// @ts-expect-error: Property 'myProp' of type 'number' is not assignable
// to string index type 'boolean'. (2411)
: number;
myProp
// @ts-expect-error: Property 'myMethod' of type '() => string' is not
// assignable to string index type 'boolean'. (2411)
myMethod(): string;
}
In contrast, the following two interfaces produce no errors:
interface I2 {: string]: number;
[key: number;
myProp
}
interface I3 {: string]: () => string;
[keymyMethod(): string;
}
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 return type Object
:
function f1(x: {}): Object {
;
return x }
Similarly, {}
has a method .toString()
:
function f2(x: {}): { toString(): string } {
;
return x }
As an example, consider the following interface:
interface Point {: number;
x: number;
y }
There are two ways (among others) in which this interface could be interpreted:
.x
and .y
with the specified types. On other words: Those objects must not have excess properties (more than the required 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:
= { x: 1, y: 2, z: 3 };
const obj computeDistance(obj); // OK
However, if we use object literals directly, then excess properties are forbidden:
// @ts-expect-error: Argument of type '{ x: number; y: number; z: number; }'
// is not assignable to parameter of type 'Point'.
// Object literal may only specify known properties, and 'z' does not
// exist in type 'Point'. (2345)
computeDistance({ x: 1, y: 2, z: 3 }); // error
computeDistance({x: 1, y: 2}); // OK
Why the stricter rules for object literals? They provide protection against typos in property keys. We will use the following interface to demonstrate what that means.
interface Person {: string;
first?: string;
middle: string;
last
}function computeFullName(person: Person) { /*...*/ }
Property .middle
is optional and can be omitted (optional properties are covered later in this chapter). 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: Argument of type '{ first: string; mdidle: string;
// last: string; }' is not assignable to parameter of type 'Person'.
// 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'});
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:
interface HasYear {: number;
year
}
function getAge(obj: HasYear) {
= new Date().getFullYear();
const yearNow - obj.year;
return yearNow }
Without allowing excess properties for most values that are passed to getAge()
, the usefulness of this function would be quite limited.
If an interface is empty (or the object type literal {}
is used), excess properties are always allowed:
interface Empty { }
interface OneProp {: number;
myProp
}
// @ts-expect-error: Type '{ myProp: number; anotherProp: number; }' is not
// assignable to type 'OneProp'.
// Object literal may only specify known properties, and
// 'anotherProp' does not exist in type 'OneProp'. (2322)
: OneProp = { myProp: 1, anotherProp: 2 };
const a: Empty = {myProp: 1, anotherProp: 2}; // OK const b
If we want to enforce that an object has no properties, we can use the following trick (credit: Geoff Goodman):
interface WithoutProperties {: string]: never;
[key
}
// @ts-expect-error: Type 'number' is not assignable to type 'never'. (2322)
: WithoutProperties = { prop: 1 };
const a: WithoutProperties = {}; // OK const b
What if we want to allow excess properties in object literals? As an example, consider interface Point
and function computeDistance1()
:
interface Point {: number;
x: number;
y
}
function computeDistance1(point: Point) { /*...*/ }
// @ts-expect-error: Argument of type '{ x: number; y: number; z: number; }'
// is not assignable to parameter of type 'Point'.
// Object literal may only specify known properties, and 'z' does not
// exist in type 'Point'. (2345)
computeDistance1({ x: 1, y: 2, z: 3 });
One option is to assign the object literal to an intermediate variable:
= { x: 1, y: 2, z: 3 };
const obj 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 interface Point
so that it allows excess properties:
interface PointEtc extends Point {: string]: any;
[key
}function computeDistance3(point: PointEtc) { /*...*/ }
computeDistance3({ x: 1, y: 2, z: 3 }); // OK
We’ll continue with two examples where TypeScript not allowing excess properties, is an issue.
Incrementor
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 {// @ts-expect-error: Type '{ counter: number; inc(): void; }' is not
// assignable to type 'Incrementor'.
// Object literal may only specify known properties, and
// 'counter' does not exist in type 'Incrementor'. (2322)
: start,
counterinc() {
// @ts-expect-error: Property 'counter' does not exist on type
// 'Incrementor'. (2339)
.counter++;
this,
};
} }
Alas, even with a type assertion, there is still one type error:
function createIncrementor2(start = 0): Incrementor {
return {: start,
counterinc() {
// @ts-expect-error: Property 'counter' does not exist on type
// 'Incrementor'. (2339)
.counter++;
this,
}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 : start,
counterinc() {
.counter++;
this,
};
};
return incrementor }
.dateStr
The following comparison function can be used to sort objects that have the property .dateStr
:
function compareDateStrings(
: {dateStr: string}, b: {dateStr: string}) {
aif (a.dateStr < b.dateStr) {
+1;
return if (a.dateStr > b.dateStr) {
} else -1;
return
} else {0;
return
} }
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.
These are the types that TypeScript infers for objects that are created via various means:
// %inferred-type: Object
= new Object();
const obj1
// %inferred-type: any
= Object.create(null);
const obj2
// %inferred-type: {}
= {};
const obj3
// %inferred-type: { prop: number; }
= {prop: 123};
const obj4
// %inferred-type: object
= Reflect.getPrototypeOf({}); const obj5
In principle, the return type of Object.create()
could be object
. However, any
allows us to add and change properties of the result.
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:
interface Name {: string;
first?: string;
middle: string;
last }
Therefore, it’s OK to omit that property (line A):
: Name = {first: 'Doe', last: 'Doe'}; // (A)
const john: Name = {first: 'Jane', middle: 'Cecily', last: 'Doe'}; const jane
undefined|string
What is the difference between .prop1
and .prop2
?
interface Interf {?: string;
prop1: undefined | string;
prop2 }
An optional property can do everything that undefined|string
can. We can even use the value undefined
for the former:
: Interf = { prop1: undefined, prop2: undefined }; const obj1
However, only .prop1
can be omitted:
: Interf = { prop2: undefined };
const obj2
// @ts-expect-error: Property 'prop2' is missing in type '{}' but required
// in type 'Interf'. (2741)
: Interf = { }; const obj3
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.
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:
: MyInterface = {
const obj: 1,
prop;
}
console.log(obj.prop); // OK
// @ts-expect-error: Cannot assign to 'prop' because it is a read-only
// property. (2540)
.prop = 2; obj
TypeScript doesn’t distinguish own and inherited properties. They are all simply considered to be properties.
interface MyInterface {toString(): string; // inherited property
: number; // own property
prop
}: MyInterface = { // OK
const obj: 123,
prop; }
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.