Object Types

#code/GYVwdgxgLglg9mABAcwE4FN1QBQAd2oDOCAXIgN6JgCGAtumYVKjGMgNyLXINUi0AjAogC+ASgoAoRIgD0smYqXLlAPXUbNW7TtXTEGKCFRIARAAl0AGytxEpxAGpE+IggB0Neu0kigA)

function greet(person: { name: string; age: number }) {
return"Hello " + person.name;
}

or they can be named by using either an interface:

Try this code ↗

interface Person {
name: string;
age: number;
}

functiongreet(person: Person) {
return"Hello " + person.name;
}

or a type alias:

Try this code ↗

type Person = {
name: string;
age: number;
};

functiongreet(person: Person) {
return"Hello " + person.name;
}

In all three examples above, we’ve written functions that take objects that contain the property name (which must be a string) and age (which must be a number).

Quick Reference

We have cheat-sheets available for both type and interface, if you want a quick look at the important every-day syntax at a glance.

Property Modifiers

Each property in an object type can specify a couple of things: the type, whether the property is optional, and whether the property can be written to.

Optional Properties

Much of the time, we’ll find ourselves dealing with objects that might have a property set. In those cases, we can mark those properties as optional by adding a question mark (?) to the end of their names.

Try this code ↗

interface PaintOptions {
shape: Shape;
xPos?: number;
yPos?: number;
}

functionpaintShape(opts: PaintOptions) {
// ...
}

constshape = getShape();
paintShape({ shape });
paintShape({ shape, xPos:100 });
paintShape({ shape, yPos:100 });
paintShape({ shape, xPos:100, yPos:100 });

In this example, both xPos and yPos are considered optional. We can choose to provide either of them, so every call above to paintShape is valid. All optionality really says is that if the property is set, it better have a specific type.

We can also read from those properties - but when we do under strictNullChecks, TypeScript will tell us they’re potentially undefined.

Try this code ↗

function paintShape(opts: PaintOptions) {
letxPos = opts.xPos;

(property) PaintOptions.xPos?: number | undefined
letyPos = opts.yPos;

(property) PaintOptions.yPos?: number | undefined
// ...
}

In JavaScript, even if the property has never been set, we can still access it - it’s just going to give us the value undefined. We can just handle undefined specially by checking for it.

Try this code ↗

function paintShape(opts: PaintOptions) {
letxPos = opts.xPos === undefined ? 0 : opts.xPos;

let xPos: number
letyPos = opts.yPos === undefined ? 0 : opts.yPos;

let yPos: number
// ...
}

Note that this pattern of setting defaults for unspecified values is so common that JavaScript has syntax to support it.

Try this code ↗

function paintShape({ shape, xPos = 0, yPos = 0 }: PaintOptions) {
console.log("x coordinate at", xPos);

(parameter) xPos: number
console.log("y coordinate at", yPos);

(parameter) yPos: number
// ...
}

Here we used a destructuring pattern ↗ for paintShape’s parameter, and provided default values ↗ for xPos and yPos. Now xPos and yPos are both definitely present within the body of paintShape, but optional for any callers to paintShape.

Note that there is currently no way to place type annotations within destructuring patterns. This is because the following syntax already means something different in JavaScript. Try this code ↗

function draw({ shape: Shape, xPos: number = 100 /*...*/ }) {
render(shape);
render(xPos);
Cannot find name 'xPos'.2304Cannot find name 'xPos'.}
Generated error
Cannot find name 'shape'. Did you mean 'Shape'?

In an object destructuring pattern, shape: Shape means “grab the property shape and redefine it locally as a variable named Shape. Likewise xPos: number creates a variable named number whose value is based on the parameter’s xPos.

readonly Properties

Properties can also be marked as readonly for TypeScript. While it won’t change any behavior at runtime, a property marked as readonly can’t be written to during type-checking.

Try this code ↗

interface SomeType {
readonlyprop: string;
}

functiondoSomething(obj: SomeType) {
// We can read from 'obj.prop'.
console.log(`prop has the value '${obj.prop}'.`);

// But we can't re-assign it.
obj.prop = "hello";
}
Generated error
Cannot assign to 'prop' because it is a read-only property.

Using the readonly modifier doesn’t necessarily imply that a value is totally immutable - or in other words, that its internal contents can’t be changed. It just means the property itself can’t be re-written to.

Try this code ↗

interface Home {
readonlyresident: { name: string; age: number };
}

functionvisitForBirthday(home: Home) {
// We can read and update properties from 'home.resident'.
console.log(`Happy birthday ${home.resident.name}!`);
home.resident.age++;
}

functionevict(home: Home) {
// But we can't write to the 'resident' property itself on a 'Home'.
home.resident = {
name:"Victor the Evictor",
age:42,
  };
}
Generated error
Cannot assign to 'resident' because it is a read-only property.

It’s important to manage expectations of what readonly implies. It’s useful to signal intent during development time for TypeScript on how an object should be used. TypeScript doesn’t factor in whether properties on two types are readonly when checking whether those types are compatible, so readonly properties can also change via aliasing.

Try this code ↗

interface Person {
name: string;
age: number;
}

interfaceReadonlyPerson {
readonlyname: string;
readonlyage: number;
}

letwritablePerson: Person = {
name:"Person McPersonface",
age:42,
};

// works
letreadonlyPerson: ReadonlyPerson = writablePerson;

console.log(readonlyPerson.age); // prints '42'
writablePerson.age++;
console.log(readonlyPerson.age); // prints '43'

Using mapping modifiers, you can remove readonly attributes.

Index Signatures

Sometimes you don’t know all the names of a type’s properties ahead of time, but you do know the shape of the values.

In those cases you can use an index signature to describe the types of possible values, for example:

Try this code ↗

interface StringArray {
  [index: number]: string;
}

constmyArray: StringArray = getStringArray();
constsecondItem = myArray[1];

const secondItem: string

Above, we have a StringArray interface which has an index signature. This index signature states that when a StringArray is indexed with a number, it will return a string.

Only some types are allowed for index signature properties: string, number, symbol, template string patterns, and union types consisting only of these.

It is possible to support both types of indexers…

It is possible to support both types of indexers, but the type returned from a numeric indexer must be a subtype of the type returned from the string indexer. This is because when indexing with a number, JavaScript will actually convert that to a string before indexing into an object. That means that indexing with 100 (a number) is the same thing as indexing with "100" (a string), so the two need to be consistent. Try this code ↗

interface Animal {
name: string;
}

interfaceDogextendsAnimal {
breed: string;
}

// Error: indexing with a numeric string might get you a completely separate type of Animal!
interfaceNotOkay {
  [x: number]: Animal;
  [x: string]: Dog;
}
Generated error
'number' index type 'Animal' is not assignable to 'string' index type 'Dog'.

While string index signatures are a powerful way to describe the “dictionary” pattern, they also enforce that all properties match their return type. This is because a string index declares that obj.property is also available as obj["property"]. In the following example, name’s type does not match the string index’s type, and the type checker gives an error:

Try this code ↗

interface NumberDictionary {
  [index: string]: number;

length: number; // ok
name: string;
}
Generated error
Property 'name' of type 'string' is not assignable to 'string' index type 'number'.

However, properties of different types are acceptable if the index signature is a union of the property types:

Try this code ↗

interface NumberOrStringDictionary {
  [index: string]: number | string;
length: number; // ok, length is a number
name: string; // ok, name is a string
}

Finally, you can make index signatures readonly in order to prevent assignment to their indices:

Try this code ↗

interface ReadonlyStringArray {
readonly [index: number]: string;
}

letmyArray: ReadonlyStringArray = getReadOnlyStringArray();
myArray[2] = "Mallory";
Generated error
Index signature in type 'ReadonlyStringArray' only permits reading.

You can’t set myArray[2] because the index signature is readonly.

Excess Property Checks

Where and how an object is assigned a type can make a difference in the type system. One of the key examples of this is in excess property checking, which validates the object more thoroughly when it is created and assigned to an object type during creation.

Try this code ↗

interface SquareConfig {
color?: string;
width?: number;
}

functioncreateSquare(config: SquareConfig): { color: string; area: number } {
return {
color:config.color || "red",
area:config.width ? config.width * config.width : 20,
  };
}

letmySquare = createSquare({ colour:"red", width:100 });
Generated error
Argument of type '{ colour: string; width: number; }' is not assignable to parameter of type 'SquareConfig'.
  Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?

Notice the given argument to createSquare is spelled colour instead of color. In plain JavaScript, this sort of thing fails silently.

You could argue that this program is correctly typed, since the width properties are compatible, there’s no color property present, and the extra colour property is insignificant.

However, TypeScript takes the stance that there’s probably a bug in this code. Object literals get special treatment and undergo excess property checking when assigning them to other variables, or passing them as arguments. If an object literal has any properties that the “target type” doesn’t have, you’ll get an error:

Try this code ↗

let mySquare = createSquare({ colour: "red", width: 100 });
Generated error
Argument of type '{ colour: string; width: number; }' is not assignable to parameter of type 'SquareConfig'.
  Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?

Getting around these checks is actually really simple. The easiest method is to just use a type assertion:

Try this code ↗

let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);

However, a better approach might be to add a string index signature if you’re sure that the object can have some extra properties that are used in some special way. If SquareConfig can have color and width properties with the above types, but could also have any number of other properties, then we could define it like so:

Try this code ↗

interface SquareConfig {
color?: string;
width?: number;
  [propName: string]: any;
}

Here we’re saying that SquareConfig can have any number of properties, and as long as they aren’t color or width, their types don’t matter.

One final way to get around these checks, which might be a bit surprising, is to assign the object to another variable: Since assigning squareOptions won’t undergo excess property checks, the compiler won’t give you an error:

Try this code ↗

let squareOptions = { colour: "red", width: 100 };
letmySquare = createSquare(squareOptions);

The above workaround will work as long as you have a common property between squareOptions and SquareConfig. In this example, it was the property width. It will however, fail if the variable does not have any common object property. For example:

Try this code ↗

let squareOptions = { colour: "red" };
letmySquare = createSquare(squareOptions);
Generated error
Type '{ colour: string; }' has no properties in common with type 'SquareConfig'.

Keep in mind that for simple code like above, you probably shouldn’t be trying to “get around” these checks. For more complex object literals that have methods and hold state, you might need to keep these techniques in mind, but a majority of excess property errors are actually bugs.

That means if you’re running into excess property checking problems for something like option bags, you might need to revise some of your type declarations. In this instance, if it’s okay to pass an object with both a color or colour property to createSquare, you should fix up the definition of SquareConfig to reflect that.

Extending Types

It’s pretty common to have types that might be more specific versions of other types. For example, we might have a BasicAddress type that describes the fields necessary for sending letters and packages in the U.S.

Try this code ↗

interface BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}

In some situations that’s enough, but addresses often have a unit number associated with them if the building at an address has multiple units. We can then describe an AddressWithUnit.

Try this code ↗

interface AddressWithUnit {
name?: string;
unit: string;
street: string;
city: string;
country: string;
postalCode: string;
}

This does the job, but the downside here is that we had to repeat all the other fields from BasicAddress when our changes were purely additive. Instead, we can extend the original BasicAddress type and just add the new fields that are unique to AddressWithUnit.

Try this code ↗

interface BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}

interfaceAddressWithUnitextendsBasicAddress {
unit: string;
}

The extends keyword on an interface allows us to effectively copy members from other named types, and add whatever new members we want. This can be useful for cutting down the amount of type declaration boilerplate we have to write, and for signaling intent that several different declarations of the same property might be related. For example, AddressWithUnit didn’t need to repeat the street property, and because street originates from BasicAddress, a reader will know that those two types are related in some way.

interfaces can also extend from multiple types.

Try this code ↗

interface Colorful {
color: string;
}

interfaceCircle {
radius: number;
}

interfaceColorfulCircleextendsColorful, Circle {}

constcc: ColorfulCircle = {
color:"red",
radius:42,
};

Intersection Types

interfaces allowed us to build up new types from other types by extending them. TypeScript provides another construct called intersection types that is mainly used to combine existing object types.

An intersection type is defined using the & operator.

Try this code ↗

interface Colorful {
color: string;
}
interfaceCircle {
radius: number;
}

typeColorfulCircle = Colorful & Circle;

Here, we’ve intersected Colorful and Circle to produce a new type that has all the members of ColorfulandCircle.

Try this code ↗

function draw(circle: Colorful & Circle) {
console.log(`Color was ${circle.color}`);
console.log(`Radius was ${circle.radius}`);
}

// okay
draw({ color:"blue", radius:42 });

// oops
draw({ color:"red", raidus:42 });
Generated error
Argument of type '{ color: string; raidus: number; }' is not assignable to parameter of type 'Colorful & Circle'.
  Object literal may only specify known properties, but 'raidus' does not exist in type 'Colorful & Circle'. Did you mean to write 'radius'?

Interfaces vs. Intersections

We just looked at two ways to combine types which are similar, but are actually subtly different. With interfaces, we could use an extends clause to extend from other types, and we were able to do something similar with intersections and name the result with a type alias. The principal difference between the two is how conflicts are handled, and that difference is typically one of the main reasons why you’d pick one over the other between an interface and a type alias of an intersection type.

Generic Object Types

Let’s imagine a Box type that can contain any value - strings, numbers, Giraffes, whatever.

Try this code ↗

interface Box {
contents: any;
}

Right now, the contents property is typed as any, which works, but can lead to accidents down the line.

We could instead use unknown, but that would mean that in cases where we already know the type of contents, we’d need to do precautionary checks, or use error-prone type assertions.

Try this code ↗

interface Box {
contents: unknown;
}

letx: Box = {
contents:"hello world",
};

// we could check 'x.contents'
if (typeofx.contents === "string") {
console.log(x.contents.toLowerCase());
}

// or we could use a type assertion
console.log((x.contentsasstring).toLowerCase());

One type safe approach would be to instead scaffold out different Box types for every type of contents.

Try this code ↗

interface NumberBox {
contents: number;
}

interfaceStringBox {
contents: string;
}

interfaceBooleanBox {
contents: boolean;
}

But that means we’ll have to create different functions, or overloads of functions, to operate on these types.

Try this code ↗

function setContents(box: StringBox, newContents: string): void;
functionsetContents(box: NumberBox, newContents: number): void;
functionsetContents(box: BooleanBox, newContents: boolean): void;
functionsetContents(box: { contents: any }, newContents: any) {
box.contents = newContents;
}

That’s a lot of boilerplate. Moreover, we might later need to introduce new types and overloads. This is frustrating, since our box types and overloads are all effectively the same.

Instead, we can make a genericBox type which declares a type parameter.

Try this code ↗

interface Box<Type> {
contents: Type;
}

You might read this as “A Box of Type is something whose contents have type Type”. Later on, when we refer to Box, we have to give a type argument in place of Type.

Try this code ↗

let box: Box<string>;

Think of Box as a template for a real type, where Type is a placeholder that will get replaced with some other type. When TypeScript sees Box<string>, it will replace every instance of Type in Box<Type> with string, and end up working with something like { contents: string }. In other words, Box<string> and our earlier StringBox work identically.

Try this code ↗

interface Box<Type> {
contents: Type;
}
interfaceStringBox {
contents: string;
}

letboxA: Box<string> = { contents:"hello" };
boxA.contents;

(property) Box<string>.contents: string

letboxB: StringBox = { contents:"world" };
boxB.contents;

(property) StringBox.contents: string

Box is reusable in that Type can be substituted with anything. That means that when we need a box for a new type, we don’t need to declare a new Box type at all (though we certainly could if we wanted to).

Try this code ↗

interface Box<Type> {
contents: Type;
}

interfaceApple {
// ....
}

// Same as '{ contents: Apple }'.
typeAppleBox = Box<Apple>;

This also means that we can avoid overloads entirely by instead using generic functions.

Try this code ↗

function setContents<Type>(box: Box<Type>, newContents: Type) {
box.contents = newContents;
}

It is worth noting that type aliases can also be generic. We could have defined our new Box<Type> interface, which was:

Try this code ↗

interface Box<Type> {
contents: Type;
}

by using a type alias instead:

Try this code ↗

type Box<Type> = {
contents: Type;
};

Since type aliases, unlike interfaces, can describe more than just object types, we can also use them to write other kinds of generic helper types.

Try this code ↗

type OrNull<Type> = Type | null;

typeOneOrMany<Type> = Type | Type[];

typeOneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;

type OneOrManyOrNull<Type> = OneOrMany<Type> | null

typeOneOrManyOrNullStrings = OneOrManyOrNull<string>;

type OneOrManyOrNullStrings = OneOrMany<string> | null

We’ll circle back to type aliases in just a little bit.

The Array Type

Generic object types are often some sort of container type that work independently of the type of elements they contain. It’s ideal for data structures to work this way so that they’re re-usable across different data types.

It turns out we’ve been working with a type just like that throughout this handbook: the Array type. Whenever we write out types like number[] or string[], that’s really just a shorthand for Array<number> and Array<string>.

Try this code ↗

function doSomething(value: Array<string>) {
// ...
}

letmyArray: string[] = ["hello", "world"];

// either of these work!
doSomething(myArray);
doSomething(newArray("hello", "world"));

Much like the Box type above, Array itself is a generic type.

Try this code ↗

interface Array<Type> {
/**
   * Gets or sets the length of the array.
   */
length: number;

/**
   * Removes the last element from an array and returns it.
   */
pop(): Type | undefined;

/**
   * Appends new elements to an array, and returns the new length of the array.
   */
push(...items: Type[]): number;

// ...
}

Modern JavaScript also provides other data structures which are generic, like Map<K, V>, Set<T>, and Promise<T>. All this really means is that because of how Map, Set, and Promise behave, they can work with any sets of types.

The ReadonlyArray Type

The ReadonlyArray is a special type that describes arrays that shouldn’t be changed.

Try this code ↗

function doStuff(values: ReadonlyArray<string>) {
// We can read from 'values'...
constcopy = values.slice();
console.log(`The first value is ${values[0]}`);

// ...but we can't mutate 'values'.
values.push("hello!");
}
Generated error
Property 'push' does not exist on type 'readonly string[]'.

Much like the readonly modifier for properties, it’s mainly a tool we can use for intent. When we see a function that returns ReadonlyArrays, it tells us we’re not meant to change the contents at all, and when we see a function that consumes ReadonlyArrays, it tells us that we can pass any array into that function without worrying that it will change its contents.

Unlike Array, there isn’t a ReadonlyArray constructor that we can use.

Try this code ↗

new ReadonlyArray("red", "green", "blue");
Generated error
'ReadonlyArray' only refers to a type, but is being used as a value here.

Instead, we can assign regular Arrays to ReadonlyArrays.

Try this code ↗

const roArray: ReadonlyArray<string> = ["red", "green", "blue"];

Just as TypeScript provides a shorthand syntax for Array<Type> with Type[], it also provides a shorthand syntax for ReadonlyArray<Type> with readonly Type[].

Try this code ↗

function doStuff(values: readonly string[]) {
// We can read from 'values'...
constcopy = values.slice();
console.log(`The first value is ${values[0]}`);

// ...but we can't mutate 'values'.
values.push("hello!");
}
Generated error
Property 'push' does not exist on type 'readonly string[]'.

One last thing to note is that unlike the readonly property modifier, assignability isn’t bidirectional between regular Arrays and ReadonlyArrays.

Try this code ↗

let x: readonly string[] = [];
lety: string[] = [];

x = y;
y = x;
Generated error
The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.

Tuple Types

A tuple type is another sort of Array type that knows exactly how many elements it contains, and exactly which types it contains at specific positions.

Try this code ↗

type StringNumberPair = [string, number];

Here, StringNumberPair is a tuple type of string and number. Like ReadonlyArray, it has no representation at runtime, but is significant to TypeScript. To the type system, StringNumberPair describes arrays whose 0 index contains a string and whose 1 index contains a number.

Try this code ↗

function doSomething(pair: [string, number]) {
consta = pair[0];

const a: string
constb = pair[1];

const b: number
// ...
}

doSomething(["hello", 42]);

If we try to index past the number of elements, we’ll get an error.

Try this code ↗

function doSomething(pair: [string, number]) {
// ...

constc = pair[2];
}
Generated error
Tuple type '[string, number]' of length '2' has no element at index '2'.

We can also destructure tuples ↗ using JavaScript’s array destructuring.

Try this code ↗

function doSomething(stringHash: [string, number]) {
const [inputString, hash] = stringHash;

console.log(inputString);

const inputString: string

console.log(hash);

const hash: number
}

Tuple types are useful in heavily convention-based APIs, where each element’s meaning is “obvious”. This gives us flexibility in whatever we want to name our variables when we destructure them. In the above example, we were able to name elements 0 and 1 to whatever we wanted. However, since not every user holds the same view of what’s obvious, it may be worth reconsidering whether using objects with descriptive property names may be better for your API.

Other than those length checks, simple tuple types like these are equivalent to types which are versions of Arrays that declare properties for specific indexes, and that declare length with a numeric literal type.

Try this code ↗

interface StringNumberPair {
// specialized properties
length: 2;
0: string;
1: number;

// Other 'Array<string | number>' members...
slice(start?: number, end?: number): Array<string | number>;
}

Another thing you may be interested in is that tuples can have optional properties by writing out a question mark (? after an element’s type). Optional tuple elements can only come at the end, and also affect the type of length.

Try this code ↗

type Either2dOr3d = [number, number, number?];

functionsetCoordinate(coord: Either2dOr3d) {
const [x, y, z] = coord;

const z: number | undefined

console.log(`Provided coordinates had ${coord.length} dimensions`);

(property) length: 2 | 3
}

Tuples can also have rest elements, which have to be an array/tuple type.

Try this code ↗

type StringNumberBooleans = [string, number, ...boolean[]];
typeStringBooleansNumber = [string, ...boolean[], number];
typeBooleansStringNumber = [...boolean[], string, number];
  • StringNumberBooleans describes a tuple whose first two elements are string and number respectively, but which may have any number of booleans following.
  • StringBooleansNumber describes a tuple whose first element is string and then any number of booleans and ending with a number.
  • BooleansStringNumber describes a tuple whose starting elements are any number of booleans and ending with a string then a number.

A tuple with a rest element has no set “length” - it only has a set of well-known elements in different positions.

Try this code ↗

const a: StringNumberBooleans = ["hello", 1];
constb: StringNumberBooleans = ["beautiful", 2, true];
constc: StringNumberBooleans = ["world", 3, true, false, true, false, true];

Why might optional and rest elements be useful? Well, it allows TypeScript to correspond tuples with parameter lists. Tuples types can be used in rest parameters and arguments, so that the following:

Try this code ↗

function readButtonInput(...args: [string, number, ...boolean[]]) {
const [name, version, ...input] = args;
// ...
}

is basically equivalent to:

Try this code ↗

function readButtonInput(name: string, version: number, ...input: boolean[]) {
// ...
}

This is handy when you want to take a variable number of arguments with a rest parameter, and you need a minimum number of elements, but you don’t want to introduce intermediate variables.

readonly Tuple Types

One final note about tuple types - tuple types have readonly variants, and can be specified by sticking a readonly modifier in front of them - just like with array shorthand syntax.

Try this code ↗

function doSomething(pair: readonly [string, number]) {
// ...
}

As you might expect, writing to any property of a readonly tuple isn’t allowed in TypeScript.

Try this code ↗

function doSomething(pair: readonly [string, number]) {
pair[0] = "hello!";
}
Generated error
Cannot assign to '0' because it is a read-only property.

Tuples tend to be created and left un-modified in most code, so annotating types as readonly tuples when possible is a good default. This is also important given that array literals with const assertions will be inferred with readonly tuple types.

Try this code ↗

let point = [3, 4] as const;

functiondistanceFromOrigin([x, y]: [number, number]) {
returnMath.sqrt(x ** 2 + y ** 2);
}

distanceFromOrigin(point);
Generated error
Argument of type 'readonly [3, 4]' is not assignable to parameter of type '[number, number]'.
  The type 'readonly [3, 4]' is 'readonly' and cannot be assigned to the mutable type '[number, number]'.

Here, distanceFromOrigin never modifies its elements, but expects a mutable tuple. Since point’s type was inferred as readonly [3, 4], it won’t be compatible with [number, number] since that type can’t guarantee point’s elements won’t be mutated.

Last updated on