In this chapter, we will explore how objects and properties are typed statically in TypeScript.
There are two ways of using objects in JavaScript:
Fixed-layout object: Used this way, an object works like a record in a database. It has a fixed number of properties, whose keys are known at development time. Their values generally have different types.
const fixedLayoutObject: FixedLayoutObjectType = {
product: 'carrot',
quantity: 4,
};
Dictionary object: Used this way, an object works like a lookup table or a map. It has a variable number of properties, whose keys are not known at development time. All of their values have the same type.
const dictionaryObject: DictionaryObjectType = {
['one']: 1,
['two']: 2,
};
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>;
FixedLayoutObjectType
is an object literal type. The separators between properties can be either commas (,
) or semicolons (;
). I prefer the former because that’s what JavaScript object literals use.
DictionaryObjectType
uses the utility type Record
to define a type for dictionary objects whose keys are strings and whose values are numbers.
Next, we’ll look at fixed-layout object types in more detail before coming back to dictionary object types.
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).
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:
Property signatures define properties and should be self-explanatory:
myProperty: boolean;
Method signatures define methods and are described in the next subsection.
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. They are described later in this chapter.
[key: string]: any;
Note: The name key
is only there for documentation purposes.
Call signatures enable object literal types to describe functions. See “Interfaces with call signatures” (§27.2.2).
(num: number): string;
Construct signatures enable object literal types to describe classes and constructor functions. See “Object type literals with construct signatures” (§23.2.3).
new(str: string): ExampleInstanceType;
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.
Just like in JavaScript, property keys can be quoted:
type Obj = { 'hello everyone!': string };
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.
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:
'abc'
123
any
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'};
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.
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 };
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;
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:
.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:
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
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'});
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.
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
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
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.
Incrementor
factoryIn 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;
}
.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.
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.
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;
}
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.
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;
}
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[]>;
}
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).
this
typesPolymorphic 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;
}
}
extends
– but type intersection (&
) is similarAn 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
.
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
}
>>;
Overriding a method means replacing a method in a supertype with a compatible method – roughly:
Object
won’t mind if the overriding method returns a RegExp
.
string
won’t mind if the overriding method accepts string | number
.
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,
};
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.
Source of this section
never
Given that no other type is assignable to never
, we can use it to forbid properties.
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 };
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'];
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).
An index signature represents an infinite set of properties; only the following types are allowed:
string
number
symbol
`${bigint}`
Specifically not allowed are:
'a'
, 1
, false
'a' | 'b'
1 | 2
boolean
(which is false | true
)
`${boolean}`
never
, any
, unknown
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,
};
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);
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,
}
Record<K, V>
for dictionary objectsThe 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.
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,
};
Record
enforces exhaustiveness for key unionsRecord
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,
};
Record
: preventing exhaustiveness checks for key unionsIf 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,
};
object
vs Object
vs. {}
These are three similar general types for objects:
object
with a lowercase “o” is the type of all non-primitive values. It’s loosely related to the value 'object'
returned by the JavaScript operator typeof
.
const obj1: object = {};
const obj2: object = [];
// @ts-expect-error: Type 'number' is not assignable to type 'object'.
const obj3: object = 123;
Object
with an uppercase “O” is the type of the instances of class Object
:
const obj1: Object = new Object();
But it also accepts primitive values (except for undefined
and null
):
const obj2: Object = 123;
Note that non-nullish primitive values inherit the methods of Object.prototype
via their wrapper types.
{}
accepts all non-nullish values. Its only difference with Object
is that it doesn’t mind if a property conflicts with Object.prototype
properties:
const obj1: {} = { toString: true }; // OK
const obj2: Object = {
// @ts-expect-error: Type 'boolean' is not assignable to
// type '() => string'.
toString: true,
};
So the type {}
basically means: “Value must not be null”.
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.
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 (that’s what instanceof
checks):
> 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
Recall that each class C
creates two entities:
C
.
C
that describes instances of the constructor function.
Similarly, there are two object types for class Object
:
Type Object
specifies the properties of instances of Object
, including the properties inherited from Object.prototype
.
Type ObjectConstructor
specifies the properties of class Object
(an object with properties).
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:
Object
(line D) and a type whose name is Object
(line A).
Object.prototype
also has the type Object
(line C). Given that any instance of Object
inherits all of its properties, that makes sense.
.valueOf()
has the return type Object
and is supposed to return primitive values.
{}
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.
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.
object
vs Object
vs. {}
vs. Record
The following table compares four types for objects:
object | Object | {} | Record |
|
---|---|---|---|---|
Accepts undefined or null | ✘ | ✘ | ✘ | ✘ |
Accepts primitive values | ✘ | ✔ | ✔ | ✘ |
Has .toString() | ✔ | ✔ | ✔ | N/A |
Values can conflict with Object | ✔ | ✘ | ✔ | N/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 }
>>,
];