In JavaScript, the fundamental way that we group and pass around data is through objects. In TypeScript, we represent those through object types.
As we’ve seen, they can be anonymous:
Try
functiongreet (person : {name : string;age : number }) {return "Hello " +person .age ;}
or they can be named by using either an interface
interfacePerson {name : string;age : number;}Try
functiongreet (person :Person ) {return "Hello " +person .age ;}
or a type alias.
typePerson = {name : string;age : number;};Try
functiongreet (person :Person ) {return "Hello " +person .age ;}
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
).
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.
interfacePaintOptions {shape :Shape ;xPos ?: number;yPos ?: number;}
functionpaintShape (opts :PaintOptions ) {// ...}Try
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.
interfacePaintOptions {shape :Shape ;xPos ?: number;yPos ?: number;}
functionpaintShape (opts :PaintOptions ) {// ...}Try
constshape =getShape ();paintShape ({shape });paintShape ({shape ,xPos : 100 });
We can also read from those properties - but when we do under strictNullChecks
, TypeScript will tell us they’re potentially undefined
.
functionpaintShape (opts :PaintOptions ) {let// ^ = (property) PaintOptions.xPos?: number | undefinedxPos =opts .xPos ;
let// ^ = (property) PaintOptions.yPos?: number | undefinedyPos =opts .yPos ;Try
// ...}
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.
functionpaintShape (opts :PaintOptions ) {let// ^ = let xPos: numberxPos =opts .xPos ===undefined ? 0 :opts .xPos ;
let// ^ = let yPos: numberyPos =opts .yPos ===undefined ? 0 :opts .yPos ;Try
// ...}
Note that this pattern of setting defaults for unspecified values is so common that JavaScript has syntax to support it.
functionpaintShape ({shape ,xPos = 0,yPos = 0 }:PaintOptions ) {// ^ = var xPos: numberconsole .log ("x coordinate at",xPos );
// ^ = var yPos: numberconsole .log ("y coordinate at",yPos );Try
// ...}
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
functiondraw ({shape :Shape ,xPos :number = 100 /*...*/ }) {Cannot find name 'shape'. Did you mean 'Shape'?2552Cannot find name 'shape'. Did you mean 'Shape'?render (); shape Cannot find name 'xPos'.2304Cannot find name 'xPos'.render (); xPos }
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.
interfaceSomeType {readonlyprop : string;}
functiondoSomething (obj :SomeType ) {// We can read from 'obj.prop'.console .log (`prop has the value '${obj .prop }'.`);Try
// But we can't re-assign it.Cannot assign to 'prop' because it is a read-only property.2540Cannot assign to 'prop' because it is a read-only property.obj .= "hello"; prop }
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.
interfaceHome {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 ++;}Try
functionevict (home :Home ) {// But we can't write to the 'resident' property itself on a 'Home'.Cannot assign to 'resident' because it is a read-only property.2540Cannot assign to 'resident' because it is a read-only property.home .= { resident name : "Victor the Evictor",age : 42,};}
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.
interfacePerson {name : string;age : number;}
interfaceReadonlyPerson {readonlyname : string;readonlyage : number;}
letwritablePerson :Person = {name : "Person McPersonface",age : 42,};
// worksletreadonlyPerson :ReadonlyPerson =writablePerson ;Try
console .log (readonlyPerson .age ); // prints '42'writablePerson .age ++;console .log (readonlyPerson .age ); // prints '43'
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
interfaceBasicAddress {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
interfaceAddressWithUnit {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
.
interfaceBasicAddress {name ?: string;street : string;city : string;country : string;postalCode : string;}Try
interfaceAddressWithUnit extendsBasicAddress {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.
interface
s can also extend from multiple types.
interfaceColorful {color : string;}
interfaceCircle {radius : number;}
interfaceColorfulCircle extendsColorful ,Circle {}Try
constcc :ColorfulCircle = {color : "red",radius : 42,};
Intersection Types
interface
s 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.
interfaceColorful {color : string;}interfaceCircle {radius : number;}Try
typeColorfulCircle =Colorful &Circle ;
Here, we’ve intersected Colorful
and Circle
to produce a new type that has all the members of Colorful
and Circle
.
functiondraw (circle :Colorful &Circle ) {console .log (`Color was ${circle .color }`);console .log (`Radius was ${circle .radius }`);}
// okaydraw ({color : "blue",radius : 42 });
// oopsArgument of type '{ color: string; raidus: number; }' is not assignable to parameter of type 'Colorful & Circle'.draw ({color : "red",raidus : 42 });Object literal may only specify known properties, but 'raidus' does not exist in type 'Colorful & Circle'. Did you mean to write 'radius'?2345Argument 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'?
Try
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 principle 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.
For example, two types can declare the same property in an interface.
TODO
Generic Object Types
Let’s imagine a Box
type that can contain any value - string
s, number
s, Giraffe
s, whatever.
Try
interfaceBox {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.
interfaceBox {contents : unknown;}
letx :Box = {contents : "hello world",};
// we could check 'x.contents'if (typeofx .contents === "string") {console .log (x .contents .toLowerCase ());}Try
// or we could use a type assertionconsole .log ((x .contents as string).toLowerCase ());
One type safe approach would be to instead scaffold out different Box
types for every type of contents
.
interfaceNumberBox {contents : number;}
interfaceStringBox {contents : string;}Try
interfaceBooleanBox {contents : boolean;}
But that means we’ll have to create different functions, or overloads of functions, to operate on these types.
Try
functionsetContents (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 generic Box
type which declares a type parameter.
Try
interfaceBox <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
letbox :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.
interfaceBox <Type > {contents :Type ;}interfaceStringBox {contents : string;}
letboxA :Box <string> = {contents : "hello" };// ^ = (property) Box<string>.contents: stringboxA .contents ;Try
letboxB :StringBox = {contents : "world" };// ^ = (property) StringBox.contents: stringboxB .contents ;
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).
interfaceBox <Type > {contents :Type ;}
interfaceApple {// ....}Try
// Same as '{ contents: Apple }'.typeAppleBox =Box <Apple >;
This also means that we can avoid overloads entirely by instead using generic functions.
Try
functionsetContents <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
interfaceBox <Type > {contents :Type ;}
by using a type alias instead:
Try
typeBox <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.
typeOrNull <Type > =Type | null;
typeOneOrMany <Type > =Type |Type [];
type// ^ = type OneOrManyOrNull<Type> = OneOrMany<Type> | nullOneOrManyOrNull <Type > =OrNull <OneOrMany <Type >>;Try
type// ^ = type OneOrManyOrNullStrings = OneOrMany<string> | nullOneOrManyOrNullStrings =OneOrManyOrNull <string>;
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>
.
functiondoSomething (value :Array <string>) {// ...}
letmyArray : string[] = ["hello", "world"];Try
// either of these work!doSomething (myArray );doSomething (newArray ("hello", "world"));
Much like the Box
type above, Array
itself is a generic type.
interfaceArray <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;Try
// ...}
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.
functiondoStuff (values :ReadonlyArray <string>) {// We can read from 'values'...constcopy =values .slice ();console .log (`The first value is ${values [0]}`);Try
// ...but we can't mutate 'values'.Property 'push' does not exist on type 'readonly string[]'.2339Property 'push' does not exist on type 'readonly string[]'.values .("hello!"); push }
Much like the readonly
modifier for properties, it’s mainly a tool we can use for intent. When we see a function that returns ReadonlyArray
s, it tells us we’re not meant to change the contents at all, and when we see a function that consumes ReadonlyArray
s, 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
new'ReadonlyArray' only refers to a type, but is being used as a value here.2693'ReadonlyArray' only refers to a type, but is being used as a value here.("red", "green", "blue"); ReadonlyArray
Instead, we can assign regular Array
s to ReadonlyArray
s.
Try
constroArray :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[]
.
functiondoStuff (values : readonly string[]) {// We can read from 'values'...constcopy =values .slice ();console .log (`The first value is ${values [0]}`);Try
// ...but we can't mutate 'values'.Property 'push' does not exist on type 'readonly string[]'.2339Property 'push' does not exist on type 'readonly string[]'.values .("hello!"); push }
One last thing to note is that unlike the readonly
property modifier, assignability isn’t bidirectional between regular Array
s and ReadonlyArray
s.
letx : readonly string[] = [];lety : string[] = [];Try
x =y ;The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.4104The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.= y x ;
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
typeStringNumberPair = [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
.
functiondoSomething (pair : [string, number]) {const// ^ = const a: stringa =pair [0];
const// ^ = const b: numberb =pair [1];
// ...}Try
doSomething (["hello", 42]);
If we try to index past the number of elements, we’ll get an error.
functiondoSomething (pair : [string, number]) {// ...Try
constTuple type '[string, number]' of length '2' has no element at index '2'.2493Tuple type '[string, number]' of length '2' has no element at index '2'.c =pair [2 ];}
We can also destructure tuples using JavaScript’s array destructuring.
functiondoSomething (stringHash : [string, number]) {const [inputString ,hash ] =stringHash ;
// ^ = const inputString: stringconsole .log (inputString );
// ^ = const hash: numberconsole .log (hash );Try
}
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
and1
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 Array
s that declare properties for specific indexes, and that declare length
with a numeric literal type.
interfaceStringNumberPair {// specialized propertieslength : 2;0: string;1: number;Try
// 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
.
typeEither2dOr3d = [number, number, number?];
functionsetCoordinate (coord :Either2dOr3d ) {const [// ^ = const z: number | undefinedx ,y ,z ] =coord ;
// ^ = (property) length: 2 | 3console .log (`Provided coordinates had ${coord .length } dimensions`);Try
}
Tuples can also have rest elements, which have to be an array/tuple type.
Try
typeStringNumberBooleans = [string, number, ...boolean[]];typeStringBooleansNumber = [string, ...boolean[], number];typeBooleansStringNumber = [...boolean[], string, number];
StringNumberBooleans
describes a tuple whose first two elements arestring
andnumber
respectively, but which may have any number ofboolean
s following.StringBooleansNumber
describes a tuple whose first element isstring
and then any number ofboolean
s and ending with anumber
.BooleansStringNumber
describes a tuple whose starting elements any number ofboolean
s and ending with astring
then anumber
.
A tuple with a rest element has no set “length” - it only has a set of well-known elements in different positions.
Try
consta :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
functionreadButtonInput (...args : [string, number, ...boolean[]]) {const [name ,version , ...input ] =args ;// ...}
is basically equivalent to:
Try
functionreadButtonInput (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 - tuples types have readonly
variants, and can be specified by sticking a readonly
modifier in front of them - just like with array shorthand syntax.
Try
functiondoSomething (pair : readonly [string, number]) {// ...}
As you might expect, writing to any property of a readonly
tuple isn’t allowed in TypeScript.
Try
functiondoSomething (pair : readonly [string, number]) {Cannot assign to '0' because it is a read-only property.2540Cannot assign to '0' because it is a read-only property.pair [0 ] = "hello!";}
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.
letpoint = [3, 4] asconst ;
functiondistanceFromOrigin ([x ,y ]: [number, number]) {returnMath .sqrt (x ** 2 +y ** 2);}
Argument of type 'readonly [3, 4]' is not assignable to parameter of type '[number, number]'.distanceFromOrigin (); point The type 'readonly [3, 4]' is 'readonly' and cannot be assigned to the mutable type '[number, number]'.2345Argument 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]'.
Try
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.