Chapter 13. An Introduction to TypeScript

Images

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.

Images Tip

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.

13.1 Type Annotations

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

Images Caution

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.

Images Note

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.

13.2 Running TypeScript

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.

Images

Figure 13-1    The TypeScript playground

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.

13.3 Type Terminology

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) => . . .

Images Note

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 as number[]
let anotherElement: typeof values[0] = 42
  // typeof values[0] is the same as number

13.4 Primitive Types

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 | '' | []

Images Note

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' }

13.5 Composite Types

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 of number
string[] // Array of string
number[][] // Array of number[]

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.

Images Note

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.

Images Note

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.

13.6 Type Inference

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.

Images Tip

To see the inferred type, use a development environment that displays type information. Figure 13-2 shows how Visual Studio Code displays inferred types.

Images

Figure 13-2 Type information in Visual Studio Code

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'
}

13.7 Subtypes

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.

13.7.1 The Substitution Rule

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.

Images Note

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

13.7.2 Optional and Excess Properties

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.

13.7.3 Array and Object Type Variance

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 a Point to a Point[]

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' of undefined

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).

13.8 Classes

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.

13.8.1 Declaring Classes

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
  . . .
}

Images Note

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)
}

Images Note

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.

Images Note

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.

13.8.2 The Instance Type of a Class

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.

Images Caution

If you only provide a setter and no getter, reading from the property is permitted and returns undefined.

13.8.3 The Static Type of a Class

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.

13.9 Structural Typing

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.

13.10 Interfaces

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.

Images Note

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.

Images Note

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 }

13.11 Indexed Properties

Images

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'

Images Caution

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 a string or string[]
  [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'

13.12 Complex Function Parameters

Images

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.

13.12.1 Optional, Default, and Rest Parameters

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

13.12.2 Destructuring Parameters

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.

13.12.3 Function Type Variance

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 for f 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 for f 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 for f 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
A variable of this type...

Subtype
...can hold a value of this type

Replace array element type with subtype

Person[]

Employee[]

Replace object property type with subtype

{ partner: Person }

{ partner: Employee }

Add object property

{ x: number, y: number }

{ x: number, y: number, color: string }

Replace function parameter type with supertype

(arg1: Employee) => void

(arg1: Person) => void

Replace function return type with subtype

(arg1: string) => Person

(arg1: string) => Employee

Drop the last parameter

(arg1: number, arg2: number) => number

(arg1: number) => number

Make the last parameter optional

(arg1: number, arg2: number) => number

(arg1: number, arg2?: number) => number

Add a rest parameter

(arg1: number) => number

(arg1: number, ...rest: number[]) => number

13.12.4 Overloads

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, '')
}

Images Caution

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, '')
  }
}

13.13 Generic Programming

Images

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.

13.13.1 Generic Classes and Types

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 }

Images Note

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]])

Images Note

The generic Array<T> class is exactly the same as the type T[].

13.13.2 Generic Functions

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.

13.13.3 Type Bounds

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>.

Images Caution

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))

13.13.4 Erasure

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.

Images Caution

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.

13.13.5 Generic Type Variance

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.

13.13.6 Conditional Types

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.

13.13.7 Mapped Types

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.

Images Caution

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.

Images Tip

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 apply length and substring to str

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]
}

Exercises

  1. What do the following types describe?

    (number | string)[]
    number[] | string[]
    [[number, string]]
    [number, string, ...:number[]]
    [number, string, ...:(number | string)[]]
    [number, ...: string[]] | [string, ...: number[]]
  2. 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?

  3. List all types of the functions of the Math class.

  4. What is the difference between the types object, Object, and {}?

  5. Describe the difference between the types

    type MaybeColoredPoint = {
      x: number,
      y: number,
      color?: string
    }

    and

    type PerhapsColoredPoint = {
      x: number,
      y: number,
      color: string | undefined
    }
  6. Given the type

    type Weekday = 'Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri' | 'Sat' | 'Sun'

    is Weekday a subtype of string or the other way around?

  7. 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 }?

  8. What is the subtype relationship between (arg: number) => void and (arg: number | undefined) => void? Between () => unknown and () => number? Between () => number and (number) => void?

  9. What is the subtype relationship between (arg1: number) => number and (arg1: number, arg2?: number) => number?

  10. 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.

  11. 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.

  12. In Section 13.11, “Indexed Properties” (page 290), you saw that it is impossible to declare

    type Dictionary = {
      created: Date, // Error—not a string or string[]
      [arg: string]: string | string[]
    }

    Can you overcome this problem with an intersection type?

  13. 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]
  14. 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.

  15. 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.

  16. 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?

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.216.230.107