Topics in This Chapter
TypeScript is a superset of JavaScript that adds compile-time typing. You annotate variables and functions with their expected types, and TypeScript reports an error whenever your code violates the type rules. Generally, that is a good thing. It is far less costly to fix compile-time errors than to debug a misbehaving program. Moreover, when you provide type information, your development tools can give you better support with autocompletion and refactoring.
This chapter contains a concise introduction into the main features of TypeScript. As with the rest of the book, I focus on modern features and mention legacy constructs only in passing. The aim of this chapter is to give you sufficient information so you can decide whether to use TypeScript on top of JavaScript.
Why wouldn’t everyone want to use TypeScript? Unlike ECMAScript, which is governed by a standards committee composed of many companies, TypeScript is produced by a single vendor, Microsoft. Unlike ECMAScript, where standards documents describe the correct behavior in mind-numbing detail, the TypeScript documentation is sketchy and inconclusive. TypeScript is—just like JavaScript—sometimes messy and inconsistent, giving you another potential source of grief and confusion. TypeScript evolves on a different schedule than ECMAScript, so there is yet another moving part. And, finally, you have yet another part in your tool chain that can act up.
You will have to weigh the advantages and drawbacks. This chapter will give you a flavor of TypeScript so you can make an informed decision.
If, after reading this chapter, you come to the conclusion that you want static type checking but you aren’t sure about TypeScript, check out Flow (https://flow.org) and see if you prefer its type system, syntax, and tooling.
Consider the following JavaScript function computing the average of two numbers:
const average = (x, y) => (x + y) / 2
What happens when you call
const result = average('3', '4')
Here, '3'
and
'4'
are concatenated to '34'
, which is then converted to the number 34
and divided by 2, yielding 17
. That is surely not what you intended.
In situations like that, JavaScript provides no error messages. The program silently computes the wrong result and keeps running. In all likelihood, something will eventually go wrong elsewhere.
In TypeScript, you annotate parameters, like this:
const average = (x: number, y: number) => (x + y) / 2
Now it is clear that the average
function is intended to compute the average of two numbers. If you call
const result = average('3', '4') // TypeScript: Compile-time error
the TypeScript compiler reports an error.
That is the promise of TypeScript: You provide type annotations, and TypeScript detects type errors before your program runs. Therefore, you spend far less time with the debugger.
In this example, the annotation process is very straightforward. Let us consider a more complex example. Suppose you want to allow an argument that is either a number or an array of numbers. In TypeScript, you express this with a union type number | number[]
. Here, we want to replace a target value, or multiple target values, with another value:
const replace = (arr: number[], target: number | number[], replacement: number) => { for (let i = 0; i < arr.length; i++) { if (Array.isArray(target) && target.includes(arr[i]) || !Array.isArray(target) && target === arr[i]) { arr[i] = replacement } } }
TypeScript can now check whether your calls are correct:
const a = [11, 12, 13, 14, 15, 16] replace(a, 13, 0) // OK replace(a, [13, 14], 0) // OK replace(a, 13, 14, 0) // Error
TypeScript knows about the types of the JavaScript library methods, but as I write this, the online playground is misconfigured and doesn’t recognize the includes
method of the Array
class. Hopefully this will be fixed by the time you read this book. If not, replace target.includes(arr[i])
with target.indexOf(arr[i]) >= 0
.
In these examples, I used arrow functions. The annotations work in exactly the same way with the function
keyword:
function average(x: number, y: number) { return (x + y) / 2 }
To use TypeScript effectively, you need to learn how to express types such as “array of type T
” and “type T
or type U
” in the TypeScript syntax. This is simple in many common situations. However, type descriptions can get fairly complex, and there are situations where you need to intervene in the typechecking process. All real-world type systems are like that. You need to expend a certain amount of upfront effort before you can reap the reward—error detection at compile time.
The easiest way to experiment with TypeScript is the “playground” at https://www.typescriptlang.org/play. Simply type in your code and run it. If you mouse over a value, its type is displayed. Errors are shown as wiggly underlines—see Figure 13-1.
Visual Studio Code (https://code.visualstudio.com/
) has excellent support for TypeScript, as do other editors and integrated development environments.
To work with TypeScript on the command line, install it with the npm
package manager. Here is the command for a global installation:
npm install -g typescript
In this chapter, I will always assume that TypeScript operates in the strict mode and targets the latest version of ECMAScript. Similar to plain JavaScript, TypeScript’s strict mode outlaws “sloppy” legacy behavior. To activate these settings, include a file tsconfig.json
in your project directory with the following contents:
{ "compilerOptions": { "target": "ES2020", "strict": true, "sourceMap": true }, "filesGlob": [ "*.ts" ] }
To compile TypeScript files to JavaScript, run
tsc
in the directory that contains TypeScript files and tsconfig.json
. Each TypeScript file is translated to JavaScript. You can run the resulting files with node
.
To start up a REPL, run
ts-node
in a directory with a tsconfig.json
file, or
ts-node -O '{ "target": "es2020", "strict": true }'
in any directory.
Let us step back and think about types. A type describes a set of values that have something in common. In TypeScript, the number
type consists of all values that are JavaScript numbers: regular numbers such as 0
, 3.141592653589793
, and so on, as well as Infinity
, -Infinity
, and NaN
. We say that all these values are instances of the number
type. However, the value 'one'
is not.
As you saw already, the type number[]
denotes arrays of numbers. The value [0, 3.141592653589793, NaN]
is an instance of the number[]
type, but the value [0, 'one']
is not.
A type such as number[]
is called a composite type. You can form arrays of any type: number[]
, string[]
, and so on. Union types are another example of composite types. The union type
number | number[]
is composed of two simpler types: number
and number[]
.
In contrast, types that are not composed of simpler types are primitive. TypeScript has primitive types number
, string
, boolean
, as well as a few others that you will encounter in the following section.
Composite types can get complex. You can use a type alias to make them easier to read and reuse. Suppose you like to write functions that accept either a single number or an array. Simply define a type alias:
type Numbers = number | number[]
Use the alias as a shortcut for the type:
const replace = (arr: number[], target: Numbers, replacement: number) => . . .
The typeof
operator yields the value of a variable or property. You can use that type to declare another variable of the same type:
let values = [1, 7, 2, 9] let moreValues: typeof values = [] //typeof values
is the same asnumber[]
let anotherElement: typeof values[0] = 42 //typeof values[0]
is the same asnumber
Every JavaScript primitive type is also a primitive type in TypeScript. That is, TypeScript has primitive types number
, boolean
, string
, symbol
, null
, and undefined
.
The undefined
type has one instance—the value undefined
. Similarly, the value null
is the sole instance of the null
type. You won’t want to use these types by themselves, but they are very useful in union types. An instance of the type
string | undefined
is either a string or the undefined
value.
The void
type can only be used as the return type of a function. It denotes the fact that the function returns no value (see Exercise 2).
The never
type denotes the fact that a function won’t ever return because it always throws an exception. Since you don’t normally write such functions, it is very unlikely that you will use the never
type for a type annotation. Section 13.13.6, “Conditional Types” (page 303), has another application of the never
type.
The unknown
type denotes any JavaScript value at all. You can convert any value to unknown
, but a value of type unknown
is not compatible with any other type. This makes sense for parameter types of very generic functions (such as console.log
), or when you need to interface with external JavaScript code. There is an even looser type any
. Any conversion to or from the any
type is allowed. You should minimize the use of the any
type because it effectively turns off type checking.
A literal value denotes another type with a single instance—that same value. For example, the string literal 'Mon'
is a TypeScript type. That type has just one value—the string 'Mon'
. By itself, such a type isn’t very useful, but you can form a union type, such as
'Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri' | 'Sat' | 'Sun'
This is a type with seven instances—the names of the weekdays.
With a type like this, you will usually want to use a type alias:
type Weekday = 'Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri' | 'Sat' | 'Sun'
Now you can annotate a variable as Weekday
:
let w: Weekday = 'Mon' // OK w = 'Mo' // Error
A type such as Weekday
describes a finite set of values. The values can be literals of any type:
type Falsish = false | 0 | 0n | null | undefined | '' | []
If you want constants with nicer names, TypeScript lets you define an enumerated type. Here is a simple example:
enum Weekday { MON, TUE, WED, THU, FRI, SAT, SUN }
You can refer to these constants as Weekday.MON
, Weekday.TUE
, and so on. These are synonyms for the numbers 0, 1, 2, 3, 4, 5, and 6. You can also assign values:
enum Color { RED = 4, GREEN = 2, BLUE = 1 }
String values are OK too:
enum Quarter { Q1 = 'Winter', Q2 = 'Spring', Q3 = 'Summer', Q4 = 'Fall' }
TypeScript provides several ways of building more complex types out of simpler ones. This section describes all of them.
Given any type, there is an array type:
number[] // Array ofnumber
string[] // Array ofstring
number[][] // Array ofnumber[]
These types describe arrays whose elements all have the same type. For example, a number[]
array can only hold numbers, not a mixture of numbers and strings.
Of course, JavaScript programmers often use arrays whose elements have mixed types, such as [404, 'not found']
. In TypeScript, you describe such an array as an instance of a tuple type [number, string]
. A tuple type is a list of types enclosed in brackets. It denotes fixed-length arrays whose elements have the specified types. In our example, the value [404, 'not found']
is an instance of the tuple type [number, string]
, but ['not found', 404]
or [404, 'error', 'not found']
are not.
The type for an array that starts out with a number and a string and then has other elements is
[string, number, ...unknown[]]
Just as a tuple type describes the element types of arrays, an object type defines the property names and types of objects. Here is an example of such a type:
{ x: number, y: number }
You can use a type alias to make this declaration easier to reuse:
type Point = { x: number, y: number }
Now you can define functions whose parameters are Point
instances:
const distanceFromOrigin = (p: Point) => Math.sqrt(Math.pow(p.x, 2) + Math.pow(p.y, 2))
A function type describes the parameter and return types of a function. For example,
(arg1: number, arg2: number) => number
is the type of all functions with two number
parameters and a number
return value.
The Math.pow
function is an instance of this type, but Math.sqrt
is not, since it only has one parameter.
In JavaScript, you must provide names with the parameter types of a function type, such as arg1
and arg2
in the preceding example. These names are ignored, with one exception. A method is indicated by naming the first parameter this
—see Section 13.8.2, “The Instance Type of a Class” (page 285). In all other cases, I will use arg1
, arg2
, and so on in a function type so you can see right away that it is a type, not an actual function. For a rest parameter, I will use rest
.
You have already seen union types. The values of the union type T | U are the instances of T or U. For example, an instance of
number | string
is either a number or a string, and
(number | string)[]
describes arrays whose elements are numbers or strings.
An intersection type T & U has instances that combine the requirements of T and U. Here is an example:
Point & { color: string }
To be an instance of this type, an object must have numeric x
and y
properties (which makes it a Point
) as well as a string-valued color
property.
Consider a call to our average
function:
const average = (x: number, y: number) => (x + y) / 2 . . . const a = 3 const b = 4 let result = average(a, b)
Only the function parameters require a type annotation. The type of the other variables is inferred. From the initialization, TypeScript can tell that a
and b
must have type number
. By analyzing the code of the average
function, TypeScript infers that the return type is also number
, and so is the type of result
.
Generally, type inference works well, but sometimes you have to help TypeScript along.
The initial value of a variable may not suffice to determine the type that you intend. For example, suppose you declare a type for error codes.
type ErrorCode = [number, string]
Now you want to declare a variable of that type. This declaration does not suffice:
let code = [404, 'not found']
TypeScript infers the type (number | string)[]
from the right-hand side: arrays of arbitrary length where each element can be a number or string. That is a much more general type than ErrorCode
.
To see the inferred type, use a development environment that displays type information. Figure 13-2 shows how Visual Studio Code displays inferred types.
The remedy is to use a type annotation with the variable:
let code: ErrorCode = [404, 'not found']
You face the same problem when a function returns a value whose type is ambiguous, such as the following:
const root = (x: number) => { if (x >= 0) return Math.sqrt(x) else return [404, 'not found'] }
The inferred return type is number | (number | string)[]
. If you want number | ErrorCode
, put a return type annotation behind the parameter list:
const root = (x: number): number | ErrorCode => { if (x >= 0) return Math.sqrt(x) else return [404, 'not found'] }
Here is the same function with the function
syntax:
function root(x: number): number | ErrorCode { if (x >= 0) return Math.sqrt(x) else return [404, 'not found'] }
A type annotation is also needed when you initialize a variable with undefined
:
let result = undefined
Without an annotation, TypeScript infers the type any
. (It would be pointless to infer the type undefined
—then the variable could never change.) Therefore, you should specify the intended type:
let result: number | undefined = undefined
Later, you can store a number in result
, but not a string:
result = 3 // OK result = '3' // Error
Sometimes you know more about the type of an expression than TypeScript can infer. For example, you might have just received a JSON object and you know its type. Then use a type assertion:
let target = JSON.parse(response) as Point
A type assertion is similar to a cast in Java or C#, but no exception occurs if the value doesn’t actually conform to the target type.
When you process union types, TypeScript follows the decision flow to ensure that a value is of the correct type in each branch. Consider this example:
const less = (x: number | number[] | string | Date | null) => { if (typeof x === 'number') return x - 1; else if (Array.isArray(x)) return x.splice(0, 1) else if (x instanceof Date) return new Date(x.getTime() - 1000) else if (x === null) return x else return x.substring(1) }
TypeScript understands the typeof
, instanceof
, and in
operators, the Array.isArray
function, and tests for null
and undefined
. Therefore, the type of x
is inferred as number
, number[]
, Date
, and null
in the first four branches. In the fifth branch, only the string
alternative remains, and TypeScript allows the call to substring
.
However, sometimes this inference doesn’t work. Here is an example:
const more = (values: number[] | string[]) => {
if (array.length > 0 && typeof x[0] === 'number') // Error—not a valid type guard
return values.map(x => x + 1)
else
return values.map(x => x + x)
}
TypeScript can’t analyze the condition. It is simply too complex.
In such a situation, you can provide a custom type guard function. Its special role is indicated by the return type:
const isNumberArray = (array: unknown[]): array is number[] => array.length > 0 && typeof array[0] === 'number'
The return type array is number[]
indicates that this function returns a boolean
and can be used to test whether the array
argument has type number[]
. Here is how to use the function:
const more = (values: number[] | string[]) => { if (isNumberArray(values)) return values.map(x => x + 1) else return values.map(x => x + x) }
Here is the same type guard with the function
syntax:
function isNumberArray(array: unknown[]): array is number[] { return array.length > 0 && typeof array[0] === 'number' }
Some types, for example number
and string
, have no relationship with each other. A number
variable cannot hold a string
variable, nor can a string
variable hold a number
value. But other types are related. For example, a variable with type number | string
can hold a number
value.
We say that number
is a subtype of number | string
, and number | string
is a supertype of number
and string
. A subtype has more constraints than its supertypes. A variable of the supertype can hold values of the subtype, but not the other way around.
In the following sections, we will examine the subtype relationship in more detail.
Consider again the object type
type Point = { x: number, y: number }
The object { x: 3, y: 4 }
is clearly an instance of Point
. What about
const bluePoint = { x: 3, y: 4, color: 'blue' }
Is it also an instance of Point
? After all, it has x
and y
properties whose values are numbers.
In TypeScript, the answer is “no.” The bluePoint
object is an instance of the type
{ x: number, y: number, color: string }
For convenience, let us give a name to that type:
type ColoredPoint = { x: number, y: number, color: string }
The ColoredPoint
type is a subtype of Point
, and Point
is a supertype of ColoredPoint
. A subtype imposes all the requirements of the supertype, and then some.
Whenever a value of a given type is expected, you can supply a subtype instance. This is sometimes called the substitution rule.
For example, here we pass a ColoredPoint
object to a function with a Point
parameter:
const distanceFromOrigin = (p: Point) => Math.sqrt(Math.pow(p.x, 2) + Math.pow(p.y, 2))
const result = distanceFromOrigin(bluePoint) // OK
The distanceFromOrigin
function expects a Point
, and it is happy to accept a ColoredPoint
. And why shouldn’t it be? The function needs to access numeric x
and y
properties, and those are certainly present.
As you just saw, the type of a variable need not be exactly the same as the type of the value to which it refers. In this example, the parameter p
has type Point
, but the value to which it refers has type ColoredPoint
. When you have a variable of a given type, you can be assured that the referenced value belongs to that type or a subtype.
The substitution rule has one exception in TypeScript. You cannot substitute an object literal of a subtype. The call
const result = distanceFromOrigin({ x: 3, y: 4, color: 'blue' }) // Error
fails at compile time. This is called an excess property check.
The same check is carried out when you assign an object literal to a typed variable:
let p: Point = { x: 3, y: 4 }
p = { x: 0, y: 0, color: 'red' } // Error—excess property blue
You will see the rationale for this check in the following section.
It is easy enough to bypass an excess property check. Just introduce another variable:
const redOrigin = { x: 0, y: 0, color: 'red' }
p = redOrigin // OK—p
can hold a subtype value
When you have an object of type Point
, you can’t read any properties other than x
and y
. After all, there is no guarantee that such properties exist.
let p: Point = . . .
console.log(p.color) // Error—no such property
That makes sense. It is exactly the kind of check that a type system should provide.
What about writing to such a property?
p.color = 'blue' // Error—no such property
From a type-theoretical point of view, this would be safe. The variable p
would still refer to a value that belongs to a subtype of Point
. But TypeScript prohibits setting “excess properties.”
If you want properties that are present with some but not all objects of a type, use optional properties. A property marked with ?
is permitted but not required. Here is an example:
type MaybeColoredPoint = { x: number, y: number, color?: string }
Now the following statements are OK:
let p: MaybeColoredPoint = { x: 0, y: 0 } // OK—color
optional
p.color = 'red' // OK—can set optional property
p = { x: 3, y: 4, color: 'blue' } // OK—can use literal with optional property
Excess property checks are meant to catch typos with optional properties. Consider a function for plotting a point:
const plot = (p: MaybeColoredPoint) => . . .
The following call fails:
const result = plot({ x: 3, y: 4, colour: 'blue' })
// Error—excess property colour
Note the British spelling of colour
. The MaybeColoredPoint
class has no colour
property, and TypeScript catches the error. If the compiler had followed the substitution rule without the excess property check, the function would have plotted a point with no color
.
Is an array of colored points more specialized than an array of points? It certainly seems to. Indeed, in TypeScript, the ColoredPoint[]
type is a subtype of Point[]
. In general, if S is a subtype of T, then the array type S[]
is a subtype of T[]
. We say that arrays are covariant in TypeScript since the array types vary in the same direction as the element types.
However, this relationship is actually unsound. It is possible to write TypeScript programs that compile without errors but create errors at runtime. Consider this example:
const coloredPoints: ColoredPoint[] = [{ x: 3, y: 4, color: 'blue' },
{ x: 0, y: 0, color: 'red' }]
const points: Point[] = coloredPoints // OK for points
to hold a subtype value
We can add a plain Point
via the points
variable:
points.push({ x: 4, y: 3 }) // OK to add aPoint
to aPoint[]
But coloredPoints
and points
refer to the same array. Reading the added point with the coloredPoints
variable causes a runtime error:
console.log(coloredPoints[2].color.length) // Error—cannot read property'length'
ofundefined
The value coloredPoints[2].color
is undefined
, which should not be possible for a ColoredPoint
. The type system has a blind spot.
This was a conscious choice by the language designers. Theoretically, only immutable arrays should be covariant, and mutable arrays should be invariant. That is, there should be no subtype relationship between mutable arrays of different types. However, invariant arrays would be inconvenient. In this case, TypeScript, as well as Java and C#, made the decision to give up on complete type safety for the sake of convenience.
Covariance is also used for object types. To determine whether one object type is a subtype of another, we look at the subtype relationships of the matching properties. Let us look at two types that share a single property:
type Colored = { color: string } type MaybeColored = { color: string | undefined }
In this case, string
is a subtype of string | undefined
, and therefore Colored
is a subtype of MaybeColored
.
In general, if S is a subtype of T, then the object type { p: S } is a subtype of { p: T }. If there are multiple properties, all of them must vary in the same direction.
As with arrays, covariance for objects is unsound—see Exercise 11.
In this section, you have seen how array and object types vary with their component types. For variance of function types, see Section 13.12.3, “Function Type Variance” (page 293), and for generic variance, Section 13.13.5, “Generic Type Variance” (page 302).
The following sections cover how classes work in TypeScript. First, we go over the syntactical differences between classes in JavaScript and TypeScript. Then you will see how classes are related to types.
The TypeScript syntax for classes is similar to that of JavaScript. Of course, you provide type annotations for constructor and method parameters. You also need to specify the types of the instance fields. One way is to list the fields with type annotations, like this:
class Point { x: number y: number constructor(x: number, y: number) { this.x = x this.y = y } distance(other: Point) { return Math.sqrt(Math.pow(this.x - other.x, 2) + Math.pow(this.y - other.y, 2)) } toString() { return `(${this.x}, ${this.y})` } static origin = new Point(0, 0) }
Alternatively, you can provide initial values from which TypeScript can infer the type:
class Point { x = 0 y = 0 . . . }
This syntax corresponds to the field syntax that is a stage 3 proposal in JavaScript.
You can make the instance fields private. TypeScript supports the syntax for private features that is currently at stage 3 in JavaScript.
class Point { #x: number #y: number constructor(x: number, y: number) { this.#x = x this.#y = y } distance(other: Point) { return Math.sqrt(Math.pow(this.#x - other.#x, 2) + Math.pow(this.#y - other.#y, 2)) } toString() { return `(${this.#x}, ${this.#y})` } static origin = new Point(0, 0) }
TypeScript also supports private
and protected
modifiers for instance fields and methods. These modifiers work just like in Java or C++. They come from a time where JavaScript did not have a syntax for private variables and methods. I do not discuss those modifiers in this chapter.
You can declare instance fields as readonly
:
class Point { readonly x: number readonly y: number . . . }
A readonly
property cannot be changed after its initial assignment.
const p = new Point(3, 4)
p.x = 0 // Error—cannot change readonly
property
Note that readonly
is applied to properties, whereas const
applies to variables.
The instances of a class have a TypeScript type that contains every public property and method. For example, consider the Point
class with public fields from the preceding sections. Its instances have the type
{ x: number, y: number, distance: (this: Point, arg1: Point) => number toString: (this: Point) => string }
Note that the constructor and static members are not a part of the instance type.
You can indicate a method by naming the first parameter this
, as in the preceding example. Alternatively, you can use the following compact notation:
{ x: number, y: number, distance(arg1: Point): number toString(): string }
Getter and setter methods in classes give rise to properties in TypeScript types. For example, if you define
get x() { return this.#x } set x(x: number) { this.#x = x } get y() { return this.#y } set y(y: number) { this.#y = y }
for the Point
class with private instance fields in the preceding section, then the TypeScript type has properties x
and y
of type number
.
If you only provide a getter, the property is readonly
.
If you only provide a setter and no getter, reading from the property is permitted and returns undefined
.
As noted in the preceding section, the constructor and static members are not part of the instance type of a class. Instead, they belong to the static type.
The static type of our sample Point
class is
{ new (x: number, y: number): Point origin: Point }
The syntax for specifying a constructor is similar to that for a method, but you use new
in place of the method name.
You don’t usually have to worry about the static type (but see Section 13.13.4, “Erasure,” page 300). Nevertheless, it is a common cause of confusion. Consider this code snippet:
const a = new Point(3, 4) const b: typeof a = new Point(0, 0) // OK const ctor: typeof Point = new Point(0, 0) // Error
Since a
is an instance of Point
, typeof a
is the instance type of the Point
class. But what is typeof Point
? Here, Point
is the constructor function. After all, that’s all a class is in JavaScript—a constructor function. Its type is the static type of the class. You can initialize ctor
as
const ctor: typeof Point = Point
Then you can call new ctor(3, 4)
or access ctor.origin
.
The TypeScript type system uses structural typing. Two types are the same if they have the same structure. For example,
type ErrorCode = [number, string]
and
type LineItem = [number, string]
are the same type. The names of the types are irrelevant. You can freely copy values between the two types:
let code: ErrorCode = [404, 'Not found'] let items: LineItem[] = [[2, 'Blackwell Toaster']] items[1] = code
This sounds potentially dangerous, but it is certainly no worse than what programmers do every day with plain JavaScript. And in practice, with object types, it is quite unlikely that two types have exactly the same structure. If we use object types in our example, we might arrive at these types:
type ErrorCode = { code: number, description: string } type LineItem = { quantity: number, description: string }
They are different since the property names don’t match.
Structural typing is very different from the “nominal” type systems in Java, C#, or C++, where the names of the type matter. But in JavaScript, what matters are the capabilities of an object, not the name of its type.
To illustrate the difference, consider this JavaScript function:
const act = x => { x.walk(); x.quack(); }
Obviously, in JavaScript, the function works with any x
that has methods walk
and quack
.
In TypeScript, you can accurately reflect this behavior with a type:
const act = (x: { walk(): void, quack(): void }) => { x.walk(); x.quack(); }
You may have a class Duck
that provides these methods:
class Duck { constructor(. . .) { . . . } walk(): void { . . . } quack(): void { . . . } }
That’s swell. You can pass a Duck
instance to the act
function:
const donald = new Duck(. . .) act(donald)
But now suppose you have another object—not an instance of this class, but still with walk
and quack
methods:
const daffy = { walk: function () { . . . }, quack: function () { . . . } };
You can equally well pass this object to the act
function. This phenomenon is called “duck typing,” after the proverbial saying: “If it walks like a duck and quacks like a duck, it must be a duck.”
The structural typing in TypeScript formalizes this approach. Using the structure of the type, TypeScript can check at compile time that each value has the needed capabilities. The type names don’t matter at all.
Consider an object type to describe objects that have an ID method:
type Identifiable = { id(): string }
Using this type, you can define a function that finds an element by ID:
const findById = (elements: Identifiable[], id: string) => { for (const e of elements) if (e.id() === id) return e; return undefined; }
To make sure that a class is a subtype of this type, you can define the class with an implements
clause:
class Person implements Identifiable { #name: string #id: string constructor(name: string, id: string) { this.#name = name; this.#id = id; } id() { return this.#id } }
Now TypeScript checks that your class really provides an id
method with the correct types.
That is all that the implements
clause does. If you omit the clause, Person
is still a subtype of Identifiable
, because of structural typing.
There is an alternate syntax for object types that looks more familiar to Java and C# programmers:
interface Identifiable { id(): string }
In older versions of TypeScript, object types were more limited than interfaces. Nowadays, you can use either.
There are a couple of minor differences. One interface can extend another:
interface Employable extends Identifiable { salary(): number }
With type declarations, you use an intersection type instead:
type Employable = Identifiable & { salary(): number }
Interfaces, unlike object types, can be defined in fragments. You can have
interface Employable { id(): string }
followed elsewhere by
interface Employable { salary(): number }
The fragments are merged together. This merging is not done for type
declarations. It is debatable whether this is a useful feature.
In TypeScript, an interface can extend a class. It then picks up all properties of the instance type of the class. For example,
interface Point3D extends Point { z: number }
has the fields and methods of Point
, as well as the z
property.
Instead of such an interface, you can use an intersection type
type Point3D = Point & { z: number }
Sometimes, you want to use objects with arbitrary properties. In TypeScript, you need to use an index signature to let the type checker know that arbitrary properties are OK. Here is the syntax:
type Dictionary = { creator: string, [arg: string]: string | string[] }
The variable name of the index argument (here, arg
) is immaterial, but you must supply a name.
Each Dictionary
instance has a creator
property and any number of other properties whose values are strings or string arrays.
const dict: Dictionary = { creator: 'Pierre' } dict.hello = ['bonjour', 'salut', 'allô'] let str = 'world' dict[str] = 'monde'
The types of explicitly provided properties must be subtypes of the index type. The following would be an error:
type Dictionary = { created: Date, // Error—not astring
orstring[]
[arg: string]: string | string[] }
There would be no way to check that an assignment to dict[str]
is correct with an arbitrary value for str
.
You can also describe array-like types with integer index values:
type ShoppingList = { created: Date, [arg: number] : string } const list: ShoppingList = { created: new Date() } list[0] = 'eggs' list[1] = 'ham'
In the following sections, you will see how to provide annotations for more optional, default, rest, and destructured parameters. Then we turn to “overloading”—specifying multiple parameter and return types for a single function.
Consider the JavaScript function
const average = (x, y) => (x + y) / 2 // JavaScript
In JavaScript, you have to worry about the fact that someone might call average(3)
, which would evaluate to (3 + undefined) / 2
, or NaN
. In TypeScript, that’s not an issue. You cannot call a function without supplying all of the required arguments.
However, JavaScript programmers often provide optional parameters. In our average
function, the second parameter can be optional:
const average = (x, y) => y === undefined ? x : (x + y) / 2 // JavaScript
In TypeScript, you tag optional parameters with a ?
, like this:
const average = (x: number, y?: number) => y === undefined ? x : (x + y) / 2
// TypeScript
Optional parameters must come after the required parameters.
As in JavaScript, you can provide default parameters in TypeScript:
const average = (x = 0, y = x) => (x + y) / 2 // TypeScript
Here, the parameter types are inferred from the types of the defaults.
Rest parameters work exactly like in JavaScript. You annotate a rest parameter as an array:
const average = (first = 0, ...following: number[]) => { let sum = first for (const value of following) { sum += value } return sum / (1 + following.length) }
The type of this function is
(arg1: number, ...arg2: number[]) => number
In Chapter 3, we looked at functions that are called with a “configuration object,” like this:
const result = mkString(elements, { separator: ', ', leftDelimiter: '(', rightDelimiter: ')' })
When implementing the function, you can, of course, have a parameter for the configuration object:
const mkString = (values, config) => config.leftDelimiter + values.join(config.separator) + config.rightDelimiter
Or you can use destructuring to declare three parameter variables:
const mkString = (values, { separator, leftDelimiter, rightDelimiter }) => leftDelimiter + values.join(separator) + rightDelimiter
In TypeScript, you need to add types. However, the obvious way does not work:
const mkString = (values: unknown[], { // TypeScript separator: string, leftDelimiter: string, // Error—duplicate identifier rightDelimiter: string // Error—duplicate identifier }) => leftDelimiter + values.join(separator) + rightDelimiter
The syntax for TypeScript type annotations is in conflict with the destructuring syntax. In JavaScript (and therefore, in TypeScript), you can add variable names after the property names:
const mkString = (values, { // JavaScript
separator: sep,
leftDelimiter: left,
rightDelimiter: right
}) => left + values.join(sep) + right
To correctly specify the types, add a type annotation to the entire configuration object:
const mkString = (values: unknown[], // TypeScript
{ separator, leftDelimiter, rightDelimiter }
: { separator: string, leftDelimiter: string, rightDelimiter: string })
=> leftDelimiter + values.join(separator) + rightDelimiter
In Chapter 3, we also provided default arguments for each option. Here is the function with the defaults:
const mkString = (values: unknown[], // TypeScript
{ separator = ',', leftDelimiter = '[', rightDelimiter = ']' }
: { separator?: string, leftDelimiter?: string, rightDelimiter?: string })
=> leftDelimiter + values.join(separator) + rightDelimiter
Note that with the defaults, the type changes slightly—each property is now optional.
In Section 13.7.3, “Array and Object Type Variance” (page 282), you saw that arrays are covariant. Replacing the element type with a subtype yields an array subtype. For example, if Employee
is a subtype of Person
, then Employee[]
is a subtype of Person[]
.
Similarly, object types are covariant in the property types. The type { partner: Employee }
is a subtype of { partner: Person }
.
In this section, we examine subtype relationships between function types.
Function types are contravariant in their parameter types. If you replace a parameter type with a supertype, you get a subtype. For example, the type
type PersonConsumer = (arg1: Person) => void
is a subtype of
type EmployeeConsumer = (arg1: Employee) => void
That means, an EmployeeConsumer
variable can hold a PersonConsumer
value:
const pc: PersonConsumer = (p: Person) => { console.log(`a person named ${p.name}`) }
const ec: EmployeeConsumer = pc
// OK for ec
to hold subtype value
This assignment is sound because pf
can surely accept Employee
instances. After all, it is prepared to handle more general Person
instances.
With the return type, we have covariance. For example,
type EmployeeProducer = (arg1: string) => Employee
is a subtype of
type PersonProducer = (arg1: string) => Person
The following assignment is sound:
const ep: EmployeeProducer = (name: string) => ({ name, salary: 0 })
const pp: PersonProducer = ep
// OK for pp
to hold subtype value
Calling pp('Fred')
surely produces a Person
instance.
If you drop the last parameter type from a function type, you obtain a subtype. For example,
(arg1: number) => number
is a subtype of
(arg1: number, arg2: number) => number
To see why, consider the assignment
const g = (x: number) => 2 * x // Type(arg1: number) => number
const f: (arg1: number, arg2: number) => number = g // OK forf
to hold subtype value
It is safe to call f
with two arguments. The second argument is simply ignored.
Similarly, if you make a parameter optional, you obtain a subtype:
const g = (x: number, y?: number) => y === undefined ? x : (x + y) / 2 // Type(arg1: number, arg2?: number) => number
const f: (arg1: number, arg2: number) => number = g // OK forf
to hold subtype value
Again, it is safe to call f
with two arguments.
Finally, if you add a rest parameter, you get a subtype.
let g = (x: number, y: number, ...following: number[]) => Math.max(x, y, ...following) // Type:(arg1: number, arg2: number, ...rest: number[]) => number
let f: (arg1: number, arg2: number) => number = g // OK forf
to hold subtype value
Once again, calling f
with two parameters is fine.
Table 13-1 gives a summary of all subtype rules that were covered so far.
Table 13-1 Forming Subtypes
Action |
Supertype |
Subtype |
---|---|---|
Replace array element type with subtype |
|
|
Replace object property type with subtype |
|
|
Add object property |
|
|
Replace function parameter type with supertype |
|
|
Replace function return type with subtype |
|
|
Drop the last parameter |
|
|
Make the last parameter optional |
|
|
Add a rest parameter |
|
|
In JavaScript, it is common to write functions that can be called flexibly. For example, this JavaScript function counts how many times a letter occurs in a string:
function count(str, c) { return str.length - str.replace(c, '').length }
What if we have an array of strings? In JavaScript, it is easy to extend the behavior:
function count(str, c) { if (Array.isArray(str)) { let sum = 0 for (const s of str) { sum += s.length - s.replace(c, '').length } return sum } else { return str.length - str.replace(c, '').length } }
In TypeScript, we need to provide a type for this function. That is not too hard: str
is either a string or an array of strings:
function count(str: string | string[], c: string) { . . . }
This works because in either case, the return type is number
. That is, the function has type
(str: string | string[], c: string) => number
But what if the return type differs depending on the argument types? Let’s say we remove the characters instead of counting them:
function remove(str, c) { // JavaScript
if (Array.isArray(str))
return str.map(s => s.replace(c, ''))
else
return str.replace(c, '')
}
Now the return type is either string
or string[]
.
But it is not optimal to use the union type string | string[]
as the return type. In an expression
const result = remove(['Fred', 'Barney'], 'e')
we would like result
to be typed as string[]
, not the union type.
You can achieve this by overloading the function. JavaScript doesn’t actually allow you to overload functions in the traditional sense—that is, implement separate functions with the same name but different parameter types. Instead, you list the declarations that you wish you could implement separately, followed by the one implementation:
function remove(str: string, c: string): string function remove(str: string[], c: string): string[] function remove(str: string | string[], c: string) { if (Array.isArray(str)) return str.map(s => s.replace(c, '')) else return str.replace(c, '') }
With arrow functions, the syntax is a little different. Annotate the type of the variable that will hold the function, like this:
const remove: { (arg1: string, arg2: string): string (arg1: string[], arg2: string): string[] } = (str: any, c: string) => { if (Array.isArray(str)) return str.map(s => s.replace(c, '')) else return str.replace(c, '') }
Perhaps for historical reasons, the syntax of this overload annotation does not use the arrow syntax for function types. Instead, the syntax is reminiscent of an interface
declaration.
Also, the type checking is not as good with arrow functions. The parameter str
must be declared with type any
, not string | string[]
. With function
declarations, TypeScript works harder and checks the execution paths of the function, guaranteeing that string
arguments yield string
results, but string[]
arguments return string[]
values.
Overloaded methods use a syntax that is similar to functions:
class Remover { c: string constructor(c: string) { this.c = c } removeFrom(str: string): string removeFrom(str: string[]): string[] removeFrom(str: string | string[]) { if (Array.isArray(str)) return str.map(s => s.replace(this.c, '')) else return str.replace(this.c, '') } }
A declaration of a class, type, or function is generic when it uses type parameters for types that are not yet specified and can be filled in later. For example, in TypeScript, the standard Set<T>
type has a type parameter T
, allowing you to form sets of any type, such as Set<string>
or Set<Point>
. The following sections show you how to work with generics in TypeScript.
Here is a simple example of a generic class. Its instances hold key/value pairs:
class Entry<K, V> { key: K value: V constructor(key: K, second: V) { this.key = key this.value = value } }
As you can see, the type parameters K
and V
are specified inside angle brackets after the name of the class. In the definitions of fields and the constructor, the type parameters are used as types.
You instantiate the generic class by substituting types for the type variables. For example, Entry<string, number>
is an ordinary class with fields of type string
and number
.
A generic type is a type with one or more type parameters, such as
type Pair<T> = { first: T, second: T }
You can specify a default for a type parameter, such as
type Pair<T = any> = { first: T, second: T }
Then the type Pair
is the same as Pair<any>
.
TypeScript provides generic forms of the Set
, Map
, and WeakMap
classes that you saw in Chapter 7. You simply provide the types of the elements:
const salaries = new Map<Person, number>()
Types can also be inferred from the constructor arguments. For example, this map is typed as a Map<string, number>
:
const weekdays = new Map( [['Mon', 0], ['Tue', 1], ['Wed', 2], ['Thu', 3], ['Fri', 4], ['Sat', 5], ['Sun', 6]])
The generic Array<T>
class is exactly the same as the type T[]
.
Just like a generic class is a class with type parameters, a generic function is a function with type parameters. Here is an example of a function with one type parameter. The function counts how many times a target value is present in an array.
function count<T>(arr: T[], target: T) { let count = 0 for (let e of arr) if (e === target) count++ return count }
Using a type parameter ensures that the array type is the same as the target type.
let digits = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5] let result = count(digits, 5) // OK result = count(digits, 'Fred') // Type error
The type parameters of a generic function are always placed before the opening parenthesis that starts the list of function parameters. A generic arrow function looks like this:
const count = <T>(arr: T[], target: T) => { let count = 0 for (let e of arr) if (e === target) count++ return count }
The type of this function is
<T> (arg1: T[], arg2: T) => number
When calling a generic function, you do not need to specify the type parameters. They are inferred from the argument types. For example, in the call count(digits, 5)
, the type of digits
is number[]
, and TypeScript can infer that T
should be number
.
You can, if you like, supply the type explicitly, before the arguments, like this:
count<number>(digits, 4)
You occasionally need to do this if TypeScript doesn’t infer the types that you intended. You will see an example in the following section.
Sometimes, the type parameters of a generic class or function need to fulfill certain requirements. You express these requirements with a type bound.
Consider this function that yields the tail—all but the first element—of its argument:
const tail = <T>(values: T) => values.slice(1) // Error
This approach cannot work since TypeScript doesn’t know whether values
has a slice
method. Let’s use a type bound:
const tail = <T extends { slice(from: number, to?: number): T }>(values: T) =>
values.slice(1) // OK
The type bound ensures that the call values.slice(1)
is valid. Note that the extends
keyword in a type bound actually means “subtype”—the TypeScript designers just used the existing extends
keyword instead of coming up with another keyword or symbol.
Now we can call
let result = tail([1, 7, 2, 9]) // Sets result to [7, 2, 9]
or
let greeting = 'Hello'
console.log(tail(greeting)) // Displays ello
Of course, we can give a name to the type that is used as a bound:
type Sliceable<T> = { slice(from: number, to?: number): T } const tail = <T extends Sliceable<T>>(values: T) => values.slice(1)
For example, the type number[]
is a subtype of Sliceable<number[]>
since the slice
method returns a number[]
instance. Similarly, string
is a subtype of Sliceable<string>
.
If you try out the call
console.log(tail('Hello')) // Error
compilation fails with an error—the type 'Hello'
is not a subtype of Sliceable<'Hello'>
. The problem is that 'Hello'
is both an instance of the literal type 'Hello'
and the type string
. TypeScript chooses the literal type as the most specific one, and typechecking fails. To overcome this problem, explicitly instantiate the template function:
console.log(tail<string>('Hello')) // OK
or use a type assertion:
console.log(tail('Hello' as string))
When TypeScript code is translated to plain JavaScript, all types are erased. As a consequence, the call
let newlyCreated = new T()
is illegal. At runtime, there is no T
.
To construct objects of arbitrary types, you need to use the constructor function. Here is an example:
const fill = <T>(ctor: { new() : T }, n: number) => { let result: T[] = [] for (let i = 0; i < n; i++) result.push(new ctor()) return result }
Note the type of ctor
—a function that can be called with new
and yields a value of type T
. That is a constructor. This particular constructor has no arguments.
When calling the fill
function, you provide the name of a class:
const dates = fill(Date, 10)
The expression Date
is the constructor function. In JavaScript, a class is just “syntactic sugar” for a constructor function with a prototype.
Similarly, you cannot make a generic instanceof
check. The following will not work:
const filter = <T>(values: unknown[]) => {
let result: T[] = []
for (const v of values)
if (v instanceof T) // Error
result.push(v)
return result
}
The remedy is, again, to pass the constructor:
const filter = <T>(values: unknown[], ctor: new (...args: any[]) => T) => {
let result: T[] = []
for (const v of values)
if (v instanceof ctor) // OK—right-hand side of instanceof
is a constructor
result.push(v)
return result
}
Here is a sample call:
const pointsOnly = filter([3, 4, new Point(3, 4), Point.origin], Point)
Note that in this case, the constructor accepts arbitrary arguments.
The instanceof
test only works with a class. There is no way of testing at runtime whether a value is an instance of a type or interface.
Consider a generic type such as
type Pair<T> = { first: T, second: T }
Now suppose you have a type Person
and a subtype Employee
. What is the appropriate relationship between Pair<Person>
and Pair<Employee>
?
Type theory provides three possibilities for a type variable. It can be covariant (that is, the generic type varies in the same direction), contravariant (with subtype relationships flipped), and invariant (with no subtype relationships between the generic types).
In Java, type variables are always invariant, but you can express relationships with wildcards such as Pair<? extends Person>
. In C#, you can choose the variance of type parameters: Entry<out K, in V>
. TypeScript does not have any comparable mechanism.
Instead, when deciding whether a generic type instance is a subtype of another, TypeScript simply substitutes the actual types and then compares the resulting nongeneric types.
For example, when comparing Pair<Person>
and Pair<Employee>
, substituting the types Person
and Employee
yields
{ first: Person, second: Person }
and the subtype
{ first: Employee, second: Employee }
As a result, the Pair<T>
type is covariant in T
. This is unsound (see Exercise 15). However, as discussed in Section 13.7.3, “Array and Object Type Variance” (page 282), this unsoundness is a conscious design decision.
Let us look at another example that illustrates contravariance:
type Formatter<T> = (arg1: T) => string
To compare Formatter<Person>
and Formatter<Employee>
, plug in the types, then compare
(arg1: Person) => string
and
(arg1: Employee) => string
Since function parameter types are contravariant, so is the type variable T
of Formatter<T>
. This behavior is sound.
A conditional type has the form T extends
U ?
V :
W, where T, U, V, and W are types or type variables. Here is an example:
type ExtractArray<T> = T extends any[] ? T : never
If T
is an array, then ExtractArray<T>
is T
itself. Otherwise, it is never
, the type that has no instances.
By itself, this type isn’t very useful. But it can be used to filter out types from unions:
type Data = string | string[] | number | number[]
type ArrayData = ExtractArray<Data> // The type string[] | number[]
For the string
and number
alternatives, ExtractArray
yields never
, which is simply dropped.
Now suppose you want to have just the element type. The following doesn’t quite work:
type ArrayOf<T> = T extends U[] ? U : never // Error—U
not defined
Instead, use the infer
keyword:
type ArrayOf<T> = T extends (infer U)[] ? U : never
Here, we check whether T
extends X[]
for some X
, and if so, bind U
to X
. When applied to a union type, the non-arrays are dropped and the arrays replaced by their element type. For example, ArrayOf<Data>
is number | string
.
Another way to specify indexes is with mapped types. Given a union type of string, integer, or symbol literals, you can define indexes like this:
type Point = { [propname in 'x'|'y']: number }
This Point
type has two properties x
and y
, both of type number
.
This notation is similar to the syntax for indexed properties (see Section 13.11, “Indexed Properties,” page 290). However, a mapped type has only one mapping, and it cannot have additional properties.
This example is not very useful. Mapped types are intended for transforming existing types. Given an Employee
type, you can make all properties readonly:
type ReadonlyEmployee = { readonly [propname in keyof Employee]: Employee[propname] }
There are two pieces of new syntax here:
The type keyof
T is the union type of all property names in T. That is 'name' | 'salary' | . . .
in this example.
The type T[p] is the type of the property with name p. For example, Employee['name']
is the type string
.
Mapped types really shine with generics. The TypeScript library defines the following utility type:
type Readonly<T> = { readonly [propname in keyof T]: T[propname] }
This type marks all properties of T
as readonly
.
By using Readonly
with a parameter type, you can assure callers that the parameter is not mutated.
const distanceFromOrigin = (p: Readonly<Point>) => Math.sqrt(Math.pow(p.x, 2) + Math.pow(p.y, 2))
Another example is the Pick
utility type that lets you pick a subset of properties, like this:
let str: Pick<string, 'length' | 'substring'> = 'Fred' // Can only applylength
andsubstring
tostr
The type is defined as follows:
type Pick<T, K extends keyof T> = { [propname in K]: T[propname] };
Note that extends
means “subtype.” The type keyof string
is the union of all string property names. A subtype is a subset of those names.
You can also remove a modifier:
type Writable<T> = { -readonly [propname in keyof T]: T[propname] }
To add or remove the ?
modifier, use ?
or -?
:
type AllRequired<T> = { [propname in keyof T]-?: T[propname] }
What do the following types describe?
(number | string)[] number[] | string[] [[number, string]] [number, string, ...:number[]] [number, string, ...:(number | string)[]] [number, ...: string[]] | [string, ...: number[]]
Investigate the difference between functions with return type void
and return type undefined
. Can a function returning void
have any return
statements? How about returning undefined
or null
? Must a function with return type undefined
have a return
statement, or can it implicitly return undefined
?
List all types of the functions of the Math
class.
What is the difference between the types object
, Object
, and {}
?
Describe the difference between the types
type MaybeColoredPoint = { x: number, y: number, color?: string }
and
type PerhapsColoredPoint = { x: number, y: number, color: string | undefined }
Given the type
type Weekday = 'Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri' | 'Sat' | 'Sun'
is Weekday
a subtype of string
or the other way around?
What is the subtype relationship between number[]
and unknown[]
? Between { x: number, y: number }
and { x: number | undefined, y: number | undefined }
? Between { x: number, y: number }
and { x: number, y: number, z: number }
?
What is the subtype relationship between (arg: number) => void
and (arg: number | undefined) => void
? Between () => unknown
and () => number
? Between () => number
and (number) => void
?
What is the subtype relationship between (arg1: number) => number
and (arg1: number, arg2?: number) => number
?
Implement the function
const act = (x: { bark(): void } | { meow(): void }) => . . .
that invokes either bark
or meow
on x
. Use the in
operator to distinguish between the alternatives.
Show that object covariance is unsound. Use the types
type Colored = { color: string } type MaybeColored = { color: string | undefined }
As with arrays in Section 13.7.3, “Array and Object Type Variance” (page 282), define two variables, one of each type, both referring to the same value. Create a situation that shows a hole in the type checker by modifying the color
property of one of the variables and reading the property with the other variable.
In Section 13.11, “Indexed Properties” (page 290), you saw that it is impossible to declare
type Dictionary = { created: Date, // Error—not astring
orstring[]
[arg: string]: string | string[] }
Can you overcome this problem with an intersection type?
Consider this type from Section 13.11, “Indexed Properties” (page 290):
type ShoppingList = { created: Date, [arg: number] : string }
Why does the following code fail?
const list: ShoppingList = { created: new Date() } list[0] = 'eggs' const more = ['ham', 'hash browns'] for (let i in arr) list[i + 1] = arr[i]
Why does this code not fail?
for (let i in arr) list[i] = arr[i]
Give an example of supertype/subtype pairs for each of the rows of Table 13-1 that is different from those given in the table. For each pair, demonstrate that a supertype variable can hold a subtype instance.
The generic Pair<T>
class from Section 13.13.5, “Generic Type Variance” (page 302), is covariant in T
. Show that this is unsound. As with arrays in Section 13.7.3, “Array and Object Type Variance” (page 282), define two variables, one of type Pair<Person>
and of type Pair<Employee>
, both referring to the same value. Mutate the value through one of the variables so that you can produce a runtime error by reading from the other variable.
Complete the generic function
const last = <. . .> (values: T) => values[values.length - 1]
so that you can call:
const str = 'Hello' console.log(last(str)) console.log(last([1, 2, 3])) console.log(last(new Int32Array(1024)))
Hint: Require that T
has a length
property and an indexed property. What is the return type of the indexed property?