Now that we know how to type simple variables and functions, let’s make things a bit more interesting with collections. In JavaScript, this means Objects and Arrays.
Objects
In general, object types are defined by:
- The names of the properties that are (or may be) present
- The types of those properties
For example, if we had the concept of a Car
like “2002 Toyota Corolla” with properties:
make
: the manufacturer (in this case, “Toyota”)model
: the particular product (in this case, “Corolla”)year
: the “model year” of the product (in this case, 2002)
We could create a JavaScript object to represent this information:
js
{make: "Toyota",model: "Corolla",year: 2002}
The type that would describe this object’s structure:
ts
{make: stringmodel: stringyear: number}
We can use this type with a variable using the same : foo
notation we’ve already discussed!
tsTry
letcar : {make : stringmodel : stringyear : number}
We could create a function to print values of this type to the console:
tsTry
/*** Print information about a car to the console* @param car - the car to print*/functionprintCar (car : {make : stringmodel : stringyear : number}) {console .log (`${car .make } ${car .model } (${car .year })`)}
Notice that we can use this exact same kind of type annotation for function arguments.
At this point, you can start to see that we see “completions” when we start
using car
in the body of this function.
tsTry
/*** Print information about a car to the console* @param car - the car to print*/functionprintCar (car : {make : stringmodel : stringyear : number}) {console .log (`${car .m }
Optional Properties
What if we take our car example a bit further by adding a fourth property that’s only present sometimes?
Property Name | Is present | Type | Note |
---|---|---|---|
make |
Always | string |
|
model |
Always | string |
|
year |
Always | number |
|
chargeVoltage |
Sometimes | number |
not present unless car is electric |
We can state that this property is optional using the ?
operator:
tsTry
functionprintCar (car : {make : stringmodel : stringyear : numberchargeVoltage ?: number}) {letstr = `${car .make } ${car .model } (${car .year })`car .chargeVoltage if (typeofcar .chargeVoltage !== "undefined")str += `// ${car .chargeVoltage }v`console .log (str )}
Note that the type of chargeVoltage
is now number | undefined
. We’ll go deeper into what the |
means, but for now you can consider it OR, for types. number | undefined
means “either number
or undefined
“.
Our printCar
function now works, regardless of whether the chargeVoltage
property is present or not:
tsTry
// WorksprintCar ({make : "Honda",model : "Accord",year : 2017,})// Also worksprintCar ({make : "Tesla",model : "Model 3",year : 2020,chargeVoltage : 220,})
Excess property checking
TypeScript helps us catch a particular type of problem around the use of object literals. Let’s look at the situation where the error arises:
tsTry
functionprintCar (car : {make : stringmodel : stringyear : numberchargeVoltage ?: number}) {// implementation removed for simplicity}printCar ({make : "Tesla",model : "Model 3",year : 2020,Object literal may only specify known properties, and 'color' does not exist in type '{ make: string; model: string; year: number; chargeVoltage?: number | undefined; }'.2353Object literal may only specify known properties, and 'color' does not exist in type '{ make: string; model: string; year: number; chargeVoltage?: number | undefined; }'.: "RED", // <0------ EXTRA PROPERTY color })
The important part of this error message is:
Object literal may only specify known properties, and ‘color’ does not exist in type <the type the function expects>
In this situation, within the body of the printCar
function, we cannot access the color
property since it’s not part
of the argument type. Thus, we’re defining a property on this object, that we have no hope of safely accessing
later on!
- Remove the
color
property from the object - Add a
color: string
to the function argument type - Create a variable to hold this value, and then pass the variable into the
printCar
function
Index signatures
Sometimes we need to represent a type for dictionaries, where values of a consistent type are retrievable by keys.
Let’s consider the following collection of phone numbers:
tsTry
constphones = {home : {country : "+1",area : "211",number : "652-4515" },work : {country : "+1",area : "670",number : "752-5856" },fax : {country : "+1",area : "322",number : "525-4357" },}
Clearly it seems that we can store phone numbers under a “key” — in this case
home
, office
, fax
, and possibly other words of our choosing — and
each phone number is comprised of three strings.
We could describe this value using what’s called an index signature:
tsTry
constphones : {[k : string]: {country : stringarea : stringnumber : string}} = {}phones .fax
Now, no matter what key we look up, we get an object that represents a phone number.
Array Types
Describing types for arrays is often as easy as adding []
to the end of the
array member’s type. For example the type for an array of string
s would look like string[]
tsTry
constfileExtensions = ["js", "ts"]
You could use our more complicated car type too, following the type for our
3-property object with []
as shown in the tooltip below:
tsTry
constcars = [{make : "Toyota",model : "Corolla",year : 2002,},]
Tuples
Sometimes we may want to work with a multi-element, ordered data structure, where position of each item has some special meaning or convention. This kind of structure is often called a tuple.
Let’s imagine we define a convention where we can represent the same “2002 Toyota Corolla” as
ts
// [Year, Make, Model ]let myCar = [2002, "Toyota", "Corolla"]// destructured assignment is convenient here!const [year, make, model] = myCar
Let’s see how TypeScript handles inference in this case:
tsTry
letmyCar = [2002, "Toyota", "Corolla"]const [year ,make ,model ] =myCar
|
means “OR”, so we can think of string | number
means either a string or a number.
TypeScript has chosen the most specific type that describes the entire contents of the array. This is not quite what we wanted, in that:
- it allows us to break our convention where the year always comes first
- it doesn’t quite help us with the “finite length” aspect of tuples
tsTry
letmyCar = [2002, "Toyota", "Corolla"]//// not the same convention or length!myCar = ["Honda", 2017, "Accord", "Sedan"]
In this case, TypeScript could infer myCar to be one of two things. Which do you think is more commonly used?
[2002, "Toyota", "Corolla"]
should be assumed to be a mixed array of numbers and strings[2002, "Toyota", "Corolla"]
should be assumed to be a tuple of fixed length (3)
Consider: Which do you use more often?
If TypeScript made a more specific assumption as it inferred the type of myCar
,
it would get in our way much of the time…
There’s no major problem here, but it does mean that we need to explicitly state the type of a tuple whenever we declare one.
tsTry
letmyCar : [number, string, string] = [2002,"Toyota","Corolla",]// ERROR: not the right conventionType 'string' is not assignable to type 'number'.Type 'number' is not assignable to type 'string'.2322myCar = ["Honda" ,2017 , "Accord"]
2322Type 'string' is not assignable to type 'number'.Type 'number' is not assignable to type 'string'.// ERROR: too many itemsType '[number, string, string, string]' is not assignable to type '[number, string, string]'. Source has 4 element(s) but target allows only 3.2322Type '[number, string, string, string]' is not assignable to type '[number, string, string]'. Source has 4 element(s) but target allows only 3.= [2017, "Honda", "Accord", "Sedan"] myCar const [year ,make ,model ] =myCar make
Now, we get errors in the places we expect, and all types work out as we hoped.
readonly
tuples
Tuples are just regular JS Array
s.
tsTry
// SourceconstnumPair : [number, number] = [4, 5];
tsTry
"use strict";// Compiled output (ES5)var numPair = [4, 5];
This imposes some degree of limitation on how tuples can be typed. For example, an Array
allows new things to be .push(...)
ed into them, allow .splice(...)
and so on. At runtime
these methods will exist on every tuple, and the types reflect that.
Typescript provides a lot of the support you’d hope for on assignment:
tsTry
constnumPair : [number, number] = [4, 5];constType '[number]' is not assignable to type '[number, number, number]'. Source has 1 element(s) but target requires 3.2322Type '[number]' is not assignable to type '[number, number, number]'. Source has 1 element(s) but target requires 3.: [number, number, number] = [7]; numTriplet
and we see something interesting happening with .length
tsTry
[101, 102, 103].length numPair .length
but we get no protection around push
and pop
, which effectively would change the type of the tuple
tsTry
numPair .push (6) // [4, 5, 6]numPair .pop () // [4, 5]numPair .pop () // [4]numPair .pop () // []numPair .length // ❌ DANGER ❌
If we are ok with treating this tuple as read-only, we can state so, and get a lot more safety around mutation.
tsTry
constroNumPair : readonly [number, number] = [4, 5]roNumPair .length Property 'push' does not exist on type 'readonly [number, number]'.2339Property 'push' does not exist on type 'readonly [number, number]'.roNumPair .(6) // [4, 5, 6] push Property 'pop' does not exist on type 'readonly [number, number]'.2339Property 'pop' does not exist on type 'readonly [number, number]'.roNumPair .() // [4, 5] pop