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!
tsTryletcar : {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:
tsTryfunctionprintCar (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:
tsTryfunctionprintCar (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
colorproperty from the object - Add a
color: stringto the function argument type - Create a variable to hold this value, and then pass the variable into the
printCarfunction
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:
tsTryconstphones = {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:
tsTryconstphones : {[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 strings would look like string[]
tsTryconstfileExtensions = ["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:
tsTryconstcars = [{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:
tsTryletmyCar = [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
tsTryletmyCar = [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.
tsTryletmyCar : [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 Arrays.
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:
tsTryconstnumPair : [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
tsTrynumPair .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.
tsTryconstroNumPair : 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