undefined|T
This chapter explains the essentials of TypeScript.
After reading this chapter, you should be able to understand the following TypeScript code:
<T> {
interface Arrayconcat(...items: Array<T[] | T>): T[];
reduce<U>(
: (state: U, element: T, index: number, array: T[]) => U,
callback?: U
firstState: U;
)// ···
}
You may think that this is cryptic. And I agree with you! But (as I hope to prove) this syntax is relatively easy to learn. And once you understand it, it gives you immediate, precise and comprehensive summaries of how code behaves – without having to read long descriptions in English.
There are many ways in which the TypeScript compiler can be configured. One important group of options controls how thoroughly the compiler checks TypeScript code. The maximum setting is activated via --strict
and I recommend to always use it. It makes programs slightly harder to write, but we also gain the full benefits of static type checking.
That’s everything about --strict
you need to know for now
Read on if you want to know more details.
Setting --strict
to true
, sets all of the following options to true
:
--noImplicitAny
: If TypeScript can’t infer a type, we must specify it. This mainly applies to parameters of functions and methods: With this settings, we must annotate them.--noImplicitThis
: Complain if the type of this
isn’t clear.--alwaysStrict
: Use JavaScript’s strict mode whenever possible.--strictNullChecks
: null
is not part of any type (other than its own type, null
) and must be explicitly mentioned if it is a acceptable value.--strictFunctionTypes
: enables stronger checks for function types.--strictPropertyInitialization
: Properties in class definitions must be initialized, unless they can have the value undefined
.We will see more compiler options later in this book, when we get to creating npm packages and web apps with TypeScript. The TypeScript handbook has comprehensive documentation on them.
In this chapter, a type is simply a set of values. The JavaScript language (not TypeScript!) has only eight types:
undefined
null
false
and true
All of these types are dynamic: we can use them at runtime.
TypeScript brings an additional layer to JavaScript: static types. These only exist when compiling or type-checking source code. Each storage location (variable, property, etc.) has a static type that predicts its dynamic values. Type checking ensures that these predictions come true.
And there is a lot that can be checked statically (without running the code). If, for example the parameter num
of a function toString(num)
has the static type number
, then the function call toString('abc')
is illegal, because the argument 'abc'
has the wrong static type.
function toString(num: number): string {
String(num);
return }
There are two type annotations in the previous function declaration:
num
: colon followed by number
toString()
: colon followed by string
Both number
and string
are type expressions that specify the types of storage locations.
Often, TypeScript can infer a static type if there is no type annotation. For example, if we omit the return type of toString()
, TypeScript infers that it is string
:
// %inferred-type: (num: number) => string
function toString(num: number) {
String(num);
return }
Type inference is not guesswork: It follows clear rules (similar to arithmetic) for deriving types where they haven’t been specified explicitly. In this case, the return statement applies a function String()
that maps arbitrary values to strings, to a value num
of type number
and returns the result. That’s why the inferred return type is string
.
If the type of a location is neither explicitly specified nor inferrable, TypeScript uses the type any
for it. This is the type of all values and a wildcard, in that we can do everything if a value has that type.
With --strict
, any
is only allowed if we use it explicitly. In other words: Every location must have an explicit or inferred static type. In the following example, parameter num
has neither and we get a compile-time error:
// @ts-expect-error: Parameter 'num' implicitly has an 'any' type. (7006)
function toString(num) {
String(num);
return }
The type expressions after the colons of type annotations range from simple to complex and are created as follows.
Basic types are valid type expressions:
undefined
, null
boolean
, number
, bigint
, string
symbol
object
.Array
(not technically a type in JavaScript)any
(the type of all values)There are many ways of combining basic types to produce new, compound types. For example, via type operators that combine types similarly to how the set operators union (∪
) and intersection (∩
) combine sets. We’ll see how to do that soon.
TypeScript has two language levels:
We can see these two levels in the syntax:
: undefined = undefined; const undef
At the dynamic level, we use JavaScript to declare a variable undef
and initialize it with the value undefined
.
At the static level, we use TypeScript to specify that variable undef
has the static type undefined
.
Note that the same syntax, undefined
, means different things depending on whether it is used at the dynamic level or at the static level.
Try to develop an awareness of the two language levels
That helps considerably with making sense of TypeScript.
With type
we can create a new name (an alias) for an existing type:
type Age = number;
: Age = 82; const age
Arrays play two roles in JavaScript (either one or both):
There are two ways to express the fact that the Array arr
is used as a list whose elements are all numbers:
: number[] = [];
let arr1: Array<number> = []; let arr2
Normally, TypeScript can infer the type of a variable if there is an assignment. In this case, we actually have to help it, because with an empty Array, it can’t determine the type of the elements.
We’ll get back to the angle brackets notation (Array<number>
) later.
If we store a two-dimensional point in an Array, then we are using that Array as a tuple. That looks as follows:
: [number, number] = [7, 5]; let point
The type annotation is needed for Arrays-as-tuples because, for Array literals, TypeScript infers list types, not tuple types:
// %inferred-type: number[]
= [7, 5]; let point
Another example for tuples is the result of Object.entries(obj)
: an Array with one [key, value] pair for each property of obj
.
// %inferred-type: [string, number][]
= Object.entries({ a: 1, b: 2 });
const entries
.deepEqual(
assert,
entries'a', 1 ], [ 'b', 2 ]]); [[
The inferred type is an Array of tuples.
This is an example of a function type:
: number) => string (num
This type comprises every function that accepts a single parameter of type number and return a string. Let’s use this type in a type annotation:
: (num: number) => string = // (A)
const toString: number) => String(num); // (B) (num
Normally, we must specify parameter types for functions. But in this case, the type of num
in line B can be inferred from the function type in line A and we can omit it:
: (num: number) => string =
const toString=> String(num); (num)
If we omit the type annotation for toString
, TypeScript infers a type from the arrow function:
// %inferred-type: (num: number) => string
= (num: number) => String(num); const toString
This time, num
must have a type annotation.
The following example is more complicated:
function stringify123(callback: (num: number) => string) {
callback(123);
return }
We are using a function type to describe the parameter callback
of stringify123()
. Due to this type annotation, TypeScript rejects the following function call.
// @ts-expect-error: Argument of type 'NumberConstructor' is not
// assignable to parameter of type '(num: number) => string'.
// Type 'number' is not assignable to type 'string'.(2345)
stringify123(Number);
But it accepts this function call:
.equal(
assertstringify123(String), '123');
TypeScript can usually infer the return types of functions, but specifying them explicitly is allowed and occasionally useful (at the very least, it doesn’t do any harm).
For stringify123()
, specifying a return type is optional and looks like this:
function stringify123(callback: (num: number) => string): string {
callback(123);
return }
void
void
is a special return type for a function: It tells TypeScript that the function always returns undefined
.
It may do so explicitly:
function f1(): void {
;
return undefined }
Or it may do so implicitly:
function f2(): void {}
However, such a function cannot explicitly return values other than undefined
:
function f3(): void {
// @ts-expect-error: Type '"abc"' is not assignable to type 'void'. (2322)
'abc';
return }
A question mark after an identifier means that the parameter is optional. For example:
function stringify123(callback?: (num: number) => string) {
if (callback === undefined) {
= String;
callback
}callback(123); // (A)
return }
TypeScript only lets us make the function call in line A if we make sure that callback
isn’t undefined
(which it is if the parameter was omitted).
TypeScript supports parameter default values:
function createPoint(x=0, y=0): [number, number] {
, y];
return [x
}
.deepEqual(
assertcreatePoint(),
0, 0]);
[.deepEqual(
assertcreatePoint(1, 2),
1, 2]); [
Default values make parameters optional. We can usually omit type annotations, because TypeScript can infer the types. For example, it can infer that x
and y
both have the type number
.
If we wanted to add type annotations, that would look as follows.
function createPoint(x:number = 0, y:number = 0): [number, number] {
, y];
return [x }
We can also use rest parameters in TypeScript parameter definitions. Their static types must be Arrays (lists or tuples):
function joinNumbers(...nums: number[]): string {
.join('-');
return nums
}.equal(
assertjoinNumbers(1, 2, 3),
'1-2-3');
The values that are held by a variable (one value at a time) may be members of different types. In that case, we need a union type. For example, in the following code, stringOrNumber
is either of type string
or of type number
:
function getScore(stringOrNumber: string|number): number {
if (typeof stringOrNumber === 'string'
&& /^\*{1,5}$/.test(stringOrNumber)) {
return stringOrNumber.length;
else if (typeof stringOrNumber === 'number'
} && stringOrNumber >= 1 && stringOrNumber <= 5) {
return stringOrNumber
else {
} throw new Error('Illegal value: ' + JSON.stringify(stringOrNumber));
}
}
.equal(getScore('*****'), 5);
assert.equal(getScore(3), 3); assert
stringOrNumber
has the type string|number
. The result of the type expression s|t
is the set-theoretic union of the types s
and t
(interpreted as sets).
undefined
and null
are not included in typesIn many programming languages, null
is part of all object types. For example, whenever the type of a variable is String
in Java, we can set it to null
and Java won’t complain.
Conversely, in TypeScript, undefined
and null
are handled by separate, disjoint types. We need union types such as undefined|string
and null|string
, if we want to allow them:
: null|number = null;
let maybeNumber= 123; maybeNumber
Otherwise, we get an error:
// @ts-expect-error: Type 'null' is not assignable to type 'number'. (2322)
: number = null;
let maybeNumber= 123; maybeNumber
Note that TypeScript does not force us to initialize immediately (as long as we don’t read from the variable before initializing it):
: number; // OK
let myNumber= 123; myNumber
Recall this function from earlier:
function stringify123(callback?: (num: number) => string) {
if (callback === undefined) {
= String;
callback
}callback(123); // (A)
return }
Let’s rewrite stringify123()
so that parameter callback
isn’t optional anymore: If a caller doesn’t want to provide a function, they must explicitly pass null
. The result looks as follows.
function stringify123(
: null | ((num: number) => string)) {
callback= 123;
const num if (callback === null) { // (A)
= String;
callback
}callback(num); // (B)
return
}
.equal(
assertstringify123(null),
'123');
// @ts-expect-error: Expected 1 arguments, but got 0. (2554)
.throws(() => stringify123()); assert
Once again, we have to handle the case of callback
not being a function (line A) before we can make the function call in line B. If we hadn’t done so, TypeScript would have reported an error in that line.
undefined|T
The following three parameter declarations are quite similar:
x?: number
x = 456
x: undefined | number
If the parameter is optional, it can be omitted. In that case, it has the value undefined
:
function f1(x?: number) { return x }
.equal(f1(123), 123); // OK
assert.equal(f1(undefined), undefined); // OK
assert.equal(f1(), undefined); // can omit assert
If the parameter has a default value, that value is used when the parameter is either omitted or set to undefined
:
function f2(x = 456) { return x }
.equal(f2(123), 123); // OK
assert.equal(f2(undefined), 456); // OK
assert.equal(f2(), 456); // can omit assert
If the parameter has a union type, it can’t be omitted, but we can set it to undefined
:
function f3(x: undefined | number) { return x }
.equal(f3(123), 123); // OK
assert.equal(f3(undefined), undefined); // OK
assert
// @ts-expect-error: Expected 1 arguments, but got 0. (2554)
f3(); // can’t omit
Similarly to Arrays, objects play two roles in JavaScript (that are occasionally mixed):
Records: A fixed number of properties that are known at development time. Each property can have a different type.
Dictionaries: An arbitrary number of properties whose names are not known at development time. All properties have the same type.
We are ignoring objects-as-dictionaries in this chapter – they are covered in §15.4.5 “Index signatures: objects as dicts”. As an aside, Maps are usually a better choice for dictionaries, anyway.
Interfaces describe objects-as-records. For example:
interface Point {: number;
x: number;
y }
We can also separate members via commas:
interface Point {: number,
x: number,
y }
One big advantage of TypeScript’s type system is that it works structurally, not nominally. That is, interface Point
matches all objects that have the appropriate structure:
interface Point {: number;
x: number;
y
}function pointToString(pt: Point) {
return `(${pt.x}, ${pt.y})`;
}
.equal(
assertpointToString({x: 5, y: 7}), // compatible structure
'(5, 7)');
Conversely, in Java’s nominal type system, we must explicitly declare with each class which interfaces it implements. Therefore, a class can only implement interfaces that exist at its creation time.
Object literal types are anonymous interfaces:
type Point = {
: number;
x: number;
y; }
One benefit of object literal types is that they can be used inline:
function pointToString(pt: {x: number, y: number}) {
return `(${pt.x}, ${pt.y})`;
}
If a property can be omitted, we put a question mark after its name:
interface Person {: string;
name?: string;
company }
In the following example, both john
and jane
match the interface Person
:
: Person = {
const john: 'John',
name;
}: Person = {
const jane: 'Jane',
name: 'Massive Dynamic',
company; }
Interfaces can also contain methods:
interface Point {: number;
x: number;
ydistance(other: Point): number;
}
As far as TypeScript’s type system is concerned, method definitions and properties whose values are functions, are equivalent:
interface HasMethodDef {simpleMethod(flag: boolean): void;
}
interface HasFuncProp {: (flag: boolean) => void;
simpleMethod
}
: HasMethodDef = {
const objWithMethodsimpleMethod(flag: boolean): void {},
;
}: HasFuncProp = objWithMethod;
const objWithMethod2
: HasMethodDef = {
const objWithOrdinaryFunction: function (flag: boolean): void {},
simpleMethod;
}: HasFuncProp = objWithOrdinaryFunction;
const objWithOrdinaryFunction2
: HasMethodDef = {
const objWithArrowFunction: (flag: boolean): void => {},
simpleMethod;
}: HasFuncProp = objWithArrowFunction; const objWithArrowFunction2
My recommendation is to use whichever syntax best expresses how a property should be set up.
Recall the two language levels of TypeScript:
Similarly:
Normal functions exist at the dynamic level, are factories for values and have parameters representing values. Parameters are declared between parentheses:
= (x: number) => x; // definition
const valueFactory = valueFactory(123); // use const myValue
Generic types exist at the static level, are factories for types and have parameters representing types. Parameters are declared between angle brackets:
type TypeFactory<X> = X; // definition
type MyType = TypeFactory<string>; // use
Naming type parameters
In TypeScript, it is common to use a single uppercase character (such as T
, I
, and O
) for a type parameter. However, any legal JavaScript identifier is allowed and longer names often make code easier to understand.
// Factory for types
<Value> {
interface ValueContainer: Value;
value
}
// Creating one type
type StringContainer = ValueContainer<string>;
Value
is a type variable. One or more type variables can be introduced between angle brackets.
Classes can have type parameters, too:
<Elem> {
class SimpleStack: Array<Elem> = [];
#datapush(x: Elem): void {
.#data.push(x);
this
}pop(): Elem {
= this.#data.pop();
const result if (result === undefined) {
new Error();
throw
};
return result
}get length() {
.#data.length;
return this
} }
Class SimpleStack
has the type parameter Elem
. When we instantiate the class, we also provide a value for the type parameter:
= new SimpleStack<string>();
const stringStack .push('first');
stringStack.push('second');
stringStack.equal(stringStack.length, 2);
assert.equal(stringStack.pop(), 'second'); assert
Maps are typed generically in TypeScript. For example:
: Map<boolean,string> = new Map([
const myMap, 'no'],
[false, 'yes'],
[true; ])
Thanks to type inference (based on the argument of new Map()
), we can omit the type parameters:
// %inferred-type: Map<boolean, string>
= new Map([
const myMap , 'no'],
[false, 'yes'],
[true; ])
Function definitions can introduce type variables like this:
function identity<Arg>(arg: Arg): Arg {
;
return arg }
We use the function as follows.
// %inferred-type: number
= identity<number>(123); const num1
Due to type inference, we can once again omit the type parameter:
// %inferred-type: 123
= identity(123); const num2
Note that TypeScript inferred the type 123
, which is a set with one number and more specific than the type number
.
Arrow functions can also have type parameters:
= <Arg>(arg: Arg): Arg => arg; const identity
This is the type parameter syntax for methods:
= {
const obj identity<Arg>(arg: Arg): Arg {
;
return arg,
}; }
function fillArray<T>(len: number, elem: T): T[] {
new Array<T>(len).fill(elem);
return }
The type variable T
appears four times in this code:
fillArray<T>
. Therefore, its scope is the function.elem
.fillArray()
.Array()
.We can omit the type parameter when calling fillArray()
(line A) because TypeScript can infer T
from the parameter elem
:
// %inferred-type: string[]
= fillArray<string>(3, '*');
const arr1 .deepEqual(
assert, ['*', '*', '*']);
arr1
// %inferred-type: string[]
= fillArray(3, '*'); // (A) const arr2
Let’s use what we have learned to understand the piece of code we have seen earlier:
<T> {
interface Arrayconcat(...items: Array<T[] | T>): T[];
reduce<U>(
: (state: U, element: T, index: number, array: T[]) => U,
callback?: U
firstState: U;
)// ···
}
This is an interface for Arrays whose elements are of type T
:
method .concat()
has zero or more parameters (defined via a rest parameter). Each of those parameters has the type T[]|T
. That is, it is either an Array of T
values or a single T
value.
method .reduce()
introduces its own type variable U
. U
is used to express the fact that the following entities all have the same type:
state
of callback()
callback()
firstState
of .reduce()
.reduce()
In addition to state
, callback()
has the following parameters:
element
, which has the same type T
as the Array elementsindex
; a numberarray
with elements of type T