Basic and built-in Types
Boolean, Number, String, Array, Enum, Void, Null, Undefined, Never, Any.
Union Types
A Union Type is created by joining multiple types via the pipe symbol |
, for example A | B.
A Union of Type A | B
means…
- the object (having this union type) must either be A or B
- in other words: the object must have at least all members of A or all members of B
- any other members that neither exist in A nor B will cause an error
type A = { age: number; name: string } type B = { skill: string; name: string; } const union: A | B = { age: 40, name: "test", skill: "", } // The following will cause an error. // Because 'name' is missing, the type is neither A nor B. const errorUnion: A | B = { age: 40, skill: "", }
So if you have an object of type A | B
you cannot really be sure whether you have an object that contains all members of A
or all members of B
. But you can be sure that the object has members that both A and B have. To get to the actual type of an object, you must narrow down the type.
In the next example we have a union type number | string
. Calling toString()
on a type string | number
works, because string
and number
both have implementations of toString()
, but calling toUpperCase()
gives an error:
function printId(id: number | string) { console.log(id.toUpperCase()); }
Property 'toUpperCase' does not exist on type 'string | number'. Property 'toUpperCase' does not exist on type 'number'.
The solution is to narrow the union with code:
function printId(id: number | string) { if (typeof id === "string") { // In this branch, id is of type 'string' console.log(id.toUpperCase()); } else { // Here, id is of type 'number' console.log(id); } }
Intersection Types
Is mainly used to combine existing object types. Intersection type A & B
means:
- must have all members of A and B
- must not have a member of another type
interface Colorful { color: string; } interface Circle { radius: number; } type ColorfulCircle = Colorful & Circle;
Type Alias and Interface
Creating a Type Alias allows you to reuse types. Instead of defining the type inline like here
function printCoord(pt: { x: number; y: number }) { }
we can create a Type Alias which allows us to reuse the type again somewhere else:
type Point = { x: number; y: number; }; function printCoord(pt: Point) { }
We could also use an interface
instead:
interface Point { x: number; y: number; }
What are the differences between Type Aliases and Interfaces?
The key distinction is that a type cannot be re-opened to add new properties vs an interface which is always extendable. Also, type aliases, unlike interfaces, can describe more than just object types, we can also use them to write other kinds of generic helper types:
type OrNull<Type> = Type | null; type OneOrMany<Type> = Type | Type[]; type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>; type OneOrManyOrNullStrings = OneOrManyOrNull<string>;
Extending an interface works:
interface Window { title: string } interface Window { ts: TypeScriptAPI } const src = 'const a = "Hello World"'; window.ts.transpileModule(src, {});
Extending a type does not work:
type Window = { title: string } type Window = { ts: TypeScriptAPI } // Error: Duplicate identifier 'Window'.
Extending types
As explained previously, types cannot be extended, but interfaces can be:
interface BasicAddress { name?: string; street: string; city: string; country: string; postalCode: string; } interface AddressWithUnit extends BasicAddress { unit: string; }
Type assertions
Sometimes you will have information about the type of a value that TypeScript can’t know about. In this situation, you can use a type assertion to specify a more specific type:
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement; // or (does not work in tsx files) const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");
Literal types
There are three sets of literal types available in TypeScript today: strings, numbers, and booleans; by using literal types you can allow an exact value which a string, number, or boolean must have.
In the following example we only allow left
, right
or center
as second parameter to printText
. We enforce that by defining literal types:
function printText(s: string, alignment: "left" | "right" | "center") { // ... } printText("Hello, world", "left"); printText("G'day, mate", "centre"); // Argument of type '"centre"' is not assignable to parameter of type '"left" | "right" | "center"'.
In the following example req.method
is inferred to be string
, but the more specific type "GET"
is expected. It is good that TypeScript considers this code to have an error because code between const req
and handleRequest
potentially could have changed req.method
to something like "GUESS"
.
const req = { url: "https://example.com", method: "GET" }; handleRequest(req.url, req.method); Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.
There are two ways to work around this.
Option 1: You can change the inference by adding a type assertion in either location:
// Change 1: const req = { url: "https://example.com", method: "GET" as "GET" }; // Change 2 handleRequest(req.url, req.method as "GET");
Option 2: You can use a as const
assertion. This tells the compiler to infer the narrowest or most specific type it can for an expression.
const req = { url: "https://example.com", method: "GET" } as const; handleRequest(req.url, req.method);
as const assertion
In addition to the previous example of using as const
, here is another one:
const args = [8, 5]; // const args: number[] const angle = Math.atan2(...args); // error! Expected 2 arguments, but got 0 or more. console.log(angle);
The problem is that Math.atan2
requires 2 arguments, and we have indeed two arguments [8, 5]
, but the compiler correctly interprets [8, 5]
as a number[]
type. Hypothetically, we could have emptied the args
array. What we need is to tell the compiler to treat the array as a const (i.e. readonly tupel) so it can assure that [8, 5]
cannot be changed to []
for example. We do exactly that when writing:
const args = [8, 5] as const; const angle = Math.atan2(...args); // works console.log(angle);
Generic Types
TypeScript’s type system is very powerful because it allows expressing types in terms of other types. The simplest form of this idea is generics, but we actually have a wide variety of type operators available to us. It’s also possible to express types in terms of values that we already have.
By combining various type operators, we can express complex operations and values in a succinct, maintainable way.
function identity<Type>(arg: Type): Type { return arg; } // possible but not required to explicitly have <string> defined here let output = identity<string>("myString"); // instead, TypeScript can infer the type itself let output = identity("myString");
Instead of
interface NumberBox { contents: number; } interface StringBox { contents: string; } interface BooleanBox { contents: boolean; }
better create a generic Box with a type parameter:
interface Box<Type> { contents: Type; }
Then use it by specifying a type:
let box: Box<string>;
Think of Box
as a template for a real type, where Type
is a placeholder that will get replaced with some other type. When TypeScript sees Box<string>
, it will replace every instance of Type
in Box<Type>
with string
, and end up working with something like { contents: string }
.
Having a generic type also means that we can avoid overloads entirely by instead using generic functions:
function setContents<Type>(box: Box<Type>, newContents: Type) { box.contents = newContents; }
Array Type
// both type definitions are the same const myArray: number[]; const myArray: Array<number>;
Modern JavaScript also provides other data structures which are generic, like Map<K, V>
, Set<T>
, and Promise<T>
.
The ReadonlyArray
is a special type that describes arrays that shouldn’t be changed.
// both type definitions are the same const roArray: ReadonlyArray<string> = ["red", "green", "blue"]; const roArray: readonly string[] = ["red", "green", "blue"];
Non-null assertion operator
Only use !
when you know that the value can’t be null
or undefined
function liveDangerously(x?: number | null) { // No error console.log(x!.toFixed()); }
Reference TS files
/// <reference path="person.ts" /> class Player implements Person { // ... }
The never
type
When narrowing, you can reduce the options of a union to a point where you have removed all possibilities and have nothing left. In those cases, TypeScript will use a never
type to represent a state which shouldn’t exist.
Some functions never return a value:
function fail(msg: string): never { throw new Error(msg); }
The unknown
type
The unknown
type represents any value. This is similar to the any
type, but is safer because it’s not legal to do anything with an unknown
value:
function f1(a: any) { a.b(); // OK } function f2(a: unknown) { a.b(); Object is of type 'unknown'. }
You can describe a function that returns a value of unknown type:
function safeParse(s: string): unknown { return JSON.parse(s); } // Need to be careful with 'obj'! const obj = safeParse(someRandomString);
The Function type
You should avoid untyped function calls like this, because of the unsafe any
return type:
function doSomething(f: Function) { f(1, 2, 3); }
If you need to accept an arbitrary function but don’t intend to call it, the type () => void
is generally safer.
Basic Conditional Type
In this example you have a function setMeta
which accepts either ImageLayer
or TextLayer
as parameter layer
, and you have another parameter meta
which accepts either ImageMeta
or TextMeta
. The problem is, that you can pass in ImageLayer
as first parameter and TextMeta
as second parameter, but what you want is that:
ImageLayer
should only acceptImageMeta
TextLayer
should only acceptTextMeta
export interface ImageMeta { origin: string; format: "png" | "jpg"; } export interface TextMeta { fontFoundry: string; licenseExpiration: Date; } function setMeta(layer: ImageLayer | TextLayer, meta: ImageMeta | TextMeta) { layer.meta = meta; } setMeta(textLayer, { format: "png", origin: "" // Problem: We passed ImageMeta to a TextLayer // but TypeScript does not complain, because setMeta function above allows this });
You can solve this in two different ways, either via function overloading or via conditional types (preferred):
With function overloading:
function setMeta(layer: ImageLayer, meta: ImageMeta): void; function setMeta(layer: TextLayer, meta: TextMeta): void; function setMeta(layer: ImageLayer | TextLayer, meta: ImageMeta | TextMeta) { layer.meta = meta; } setMeta(textLayer, { format: "png", origin: "" // Good, now TypeScript complains that TextLayer was given ImageMeta });
setMeta(textLayer, { fontFoundry: "something", licenseExpiration: new Date() // It works, but IntelliSense still offers ImageMeta (which we cannot use) });
The preferred solution is to use Conditional Types. First remove the overloaded functions, then add:
function setMeta<T extends TextLayer | ImageLayer>( layer: T, meta: T extends TextLayer ? TextMeta : ImageMeta) { layer.meta = meta; } setMeta(imageLayer, { format: "png", origin: "" // Works, and also great: IntelliSense only offers ImageMeta properties here // If the first param was textLayer, then IntelliSense offered TextMeta properties here });
Function Type Expressions
// The syntax (a: string) => void means “a function with one parameter, named a, of type string, that doesn’t have a return value” function greeter(fn: (a: string) => void) { fn("Hello, World"); } function printToConsole(s: string) { console.log(s); } greeter(printToConsole);
Call Signatures
In JavaScript Functions can be callable and can additionally have properties:
type DescribableFunction = { description: string; (someArg: number): boolean; }; function doSomething(fn: DescribableFunction) { console.log(fn.description + " returned " + fn(6)); }
Note that the syntax is slightly different compared to a function type expression
Construct Signatures
You can write a construct signature by adding the new
keyword in front of a call signature:
type SomeConstructor = { new (s: string): SomeObject; }; function fn(ctor: SomeConstructor) { return new ctor("hello"); }
Index Signatures
Sometimes you don’t know all the names of a type’s properties ahead of time, but you do know the shape of the values. An index signature property type must be either ‘string’ or ‘number’.
interface Dictionary<T> { [key: string]: T; }
Conditional return types
function createLayer(type: LayerType): TextLayer | ImageLayer { if (type === LayerType.Text) { return { color: "#fff", fontSize: "10px", id: new Date().toISOString(), maxWidth: 10000, position: { x: 10, y: 10 }, rotation: 0, text: "This is the default text", type: LayerType.Text } as TextLayer; } return { id: new Date().toISOString(), maxBounds: { width: 400 }, position: { x: 0, y: 0 }, rotation: 0, src: "ps-dark.png", type: LayerType.Image } as ImageLayer; } const textLayer = createLayer(LayerType.Text); // Problem: textLayer can be TextLayer or ImageLayer which (if ImageLayer) does not have text property textLayer.text = "can't set this";
To fix this, we have to adjust the return type of createLayer function:
function createLayer<T extends LayerType>(type: T): T extends LayerType.Text ? TextLayer : ImageLayer { if (type === LayerType.Text) { return { color: "#fff", fontSize: "10px", id: new Date().toISOString(), maxWidth: 10000, position: { x: 10, y: 10 }, rotation: 0, text: "This is the default text", type: LayerType.Text } as T extends LayerType.Text ? TextLayer : ImageLayer; } return { id: new Date().toISOString(), maxBounds: { width: 400 }, position: { x: 0, y: 0 }, rotation: 0, src: "ps-dark.png", type: LayerType.Image } as T extends LayerType.Text ? TextLayer : ImageLayer; } const textLayer = createLayer(LayerType.Text); textLayer.text = "now it can be set";
Now it works. To make it cleaner, you can refactor out the conditional return type to its own type, in this case FactoryLayer
:
type FactoryLayer<T> = T extends LayerType.Text ? TextLayer : ImageLayer; function createLayer<T extends LayerType>(type: T): FactoryLayer<T> { if (type === LayerType.Text) { return { // ... } as FactoryLayer<T>; } return { // ... } as FactoryLayer<T>; }
Lookup types
keyof
Lookup types, also referred to as indexed access types enable us to access the keys or the types for keys. Similar to how we can access the values of object properties, but for types.
type User = { id: number; name: string; place: string } type UserKeyTypes = User[keyof User]; // string | number type UserKeys = keyof User; // "id", "name", "place" const works: UserKeys = "name"; const worksToo: UserKeys = "id"; const doesNotWork: UserKeys = 4711;
Mapping from one type to another
We want to ensure that you have all the properties of an existing type in another object:
const fieldDescriptions = { text: "This is the default text", anything: true // we don't want to allow this }; Object.entries(fieldDescriptions).forEach(([field, description]) => { console.log(`${field}: ${description}`); });
Solution 1:
type LayerCombined = TextLayer & ImageLayer; type FieldDescriptions = { [key in keyof LayerCombined]: string; } const fieldDescriptions: FieldDescriptions = { text: "This is the default text", anything: true // Good, TypeScript complains that 'anything' does not exist in FieldDescriptions (that is: TextLayer) // Also good, TypeScript complains that other properties from FieldDescriptions (that is: TextLayer) are not specified here }; Object.entries(fieldDescriptions).forEach(([field, description]) => { console.log(`${field}: ${description}`); });
If you want to ensure all properties of two types, such as TextLayer and ImageLayer:
type FieldDescriptions = { [key in keyof (TextLayer & ImageLayer)]: string; }
Only some of the properties of a type
Or in other words: All properties, except the ignored ones:
import { Project, TextLayer, ImageLayer, LayerType, Size } from "./types"; type LayerCombined = TextLayer & ImageLayer; type IgnoredProperties = "id" | "maxBounds" | "position" | "meta"; type FieldDescriptions = { [key in Exclude<keyof LayerCombined, IgnoredProperties>]: string; } const fieldDescriptions: FieldDescriptions = { text: "This is the default text", // Good, typescript complains that 'anything' does not exist in TextLayer }; Object.entries(fieldDescriptions).forEach(([field, description]) => { console.log(`${field}: ${description}`); });
Mixins
If a class is getting too big and has too many responsibilities it should be split up. One way to do this is using Mixins. A Mixin is a function that returns a class. Let’s say you have a class ApiClient that has methods responsible for Projects and Images and you want to split out those methods to two mixins ProjectsAPI
and ImagesAPI
. But you still want to reference those methods through the class ApiClient
:
# ProjectsAPI and ImagesAPI are functions (the Mixins) that each return a class # Wrap them like this and you get both their functionalities class ApiClient extends ProjectsAPI(ImagesAPI(CoreAPI)) {}
Now you can use methods related to Projects and Images in ApiClient. But the questions is of course, how do we code the Mixins themselves. This way:
export class CoreAPI { constructor(public baseUrl: string) {} } export type BaseAPIConstructor = new (...args: any[]) => CoreAPI; function ImagesAPI<TBase extends BaseAPIConstructor>(Base: TBase) { return class extends Base { getImages(): Promise<{ src: string; dimensions: Size; }> { return Promise.resolve({ src: "http://a-fake-url.com/image-id", dimensions: { height: 100, width: 300 } }); } saveImage(src: string): Promise<number> { console.log(this.baseUrl); return Promise.resolve(10); } }; } function ProjectsAPI<TBase extends BaseAPIConstructor>(Base: TBase) { return class extends Base { getProjects(): Promise<Project[]> { return Promise.resolve([]); } saveProject(): Promise<string> { return Promise.resolve("1"); } deleteProject(): Promise<void> { return Promise.resolve(); } }; }
Tuples
Tuples enable you to describe what type will be at what index:
type NumNumStrTuple = [number, number, string]; const myTuple: NumNumStrTuple = [4, 1, "hey"]; const myBadTuple: NumNumStrTuple = ["4", 1, "hey"]; // Error: "4" is not a number const myOtherBadTuple: NumNumStrTuple = [4, 1]; // Error: missing string on third position