Intermediate TypeScript

This is a summary of https://frontendmasters.com/courses/intermediate-typescript/.

Table of Contents

Intro

By the end of the course, we will understand the snippet below:

// Get keys of type T whose values are assignable to type U type FilteredKeys<T, U> = { [P in keyof T]: T[P] extends U ? P : never }[keyof T] & keyof T /** * get a subset of Document, consisting only of methods * returning an Element (e.g., querySelector) or an * Element[] (e.g., querySelectorAll) */ type ValueFilteredDoc = Pick< Document, FilteredKeys< Document, (...args: any[]) => Element | Element[] > >

We must have already completed TypeScript Fundamentals.

Declaration Merging

Type and value can be identified by a single name. What does that mean?

interface Fruit { name: string mass: number color: string } const Fruit: Fruit { name: "Apple" mass: 1, color: "red" } export { Fruit }

Class can be thought of as a type or a value:

class Person { constructor( public name: string, public age: number, public address: string, ) {} static sayHello() { console.log("Hello!"); } public greeting(): void { console.log(`Hi, ${name}`); } } const person1: Person = new Person('Tatang', 21, 'New York'); person1.greeting(); const bluePrint = Person; bluePrint.sayHello();

Modules & CJS Interop

The JS ecosystem was without an “official” module specification until 2015. There are:

  • CommonJS
  • AMD (RequireJS)
  • UMD

Node.js still use CommonJS.

The way TypeScript and JavaScript imports and exports modules are the same:

export default Share; export { json } from './promise'; export * from './hello'; export const hi = () => console.log('Hi!'); export * as greeting from './greeting'; import Share from './share'; import * as greeting from './greeting'; import { json } from './promise';

Sometimes we want to import CommonJS module to our project:

// Turn this: const fs = require('fs'); // Into this: import * as fs from 'fs';

But occasionally, you’ll run into a rare situation where the CommonJS module you’re importing from, exports a single thing that’s incompatible with this namespace import technique:

// @filename 'greeting.ts' function greeting() { console.log("Hello there!"); } export = greeting; // @filename 'index.ts' import * as greeting from './greeting'; greeting();

The workaround is to enable esModuleInterop and allowSyntheticDefaultImports.

To overcome that without enabling esModuleInterop and allowSyntheticDefaultImpots, we must import the module this way:

import greeting = require('./greeting'); greeting();

With Webpack (or other bundlers), we can import image file this way:

import logo from './logo.png';

But TypeScript will complain about it. So, we must write module declaration for that:

declare module '*.png' { const imageUrl: string export default imageUrl }

Type Queries

If we want to obtain a type representing properties of an object, we can use keyof operator:

interface Person { name: string; age: number; }; type keys = keyof Person;

If we hover over keys, it’s still keyof Person. Try to append & string and it will display a union of Person‘s properties ("name" | "age"):

type keys = keyof Person & string; // "name" | "age"

keyof types become especially useful when combined with mapped types.

typeof will return the type of a value. Remember, in JavaScript you can ask the type of a value with typeof too:

async function main() { const response = await Promise.all( fetch('https://github.com'), Promise.resove(2) ); type ResponseType = typeof response; }

typeof also represents the blueprint of an object (the class itself):

class Person { constructor(public name: string) {} static greet() { console.log('hello'); } } const theClassItself: typeof Person = Person; const person: Person = new Person('Tatang'); theClassItself.greet();

Conditional Types

We are used to ternary operation in JavaScript:

const bla = ifTrue() ? 'bla' : 'boo'

TypeScript’s conditional types work the same way:

interface Car { type: "car"; brand: string; doors: number; } interface Motorbike { type: "motorbike"; brand: string; engineCapacity: number; } interface Boat { type: "boat"; brand: string; length: number; } interface Airplane { type: "airplane"; brand: string; wingspan: number; } type Vehicle = Car | Motorbike | Boat | Airplane; type VehicleInfo<T extends Vehicle> = T extends Car ? `This is a car with ${T["doors"]} doors.` : T extends Motorbike ? `This is a motorbike with an engine capacity of ${T["engineCapacity"]} cc.` : T extends Boat ? `This is a boat with a length of ${T["length"]} meters.` : T extends Airplane ? `This is an airplane with a wingspan of ${T["wingspan"]} meters.` : "Unknown vehicle type."; function getVehicleInfo<T extends Vehicle>(vehicle: T): VehicleInfo<T> { if (vehicle.type === "car") { return `This is a car with ${vehicle.doors} doors.` as VehicleInfo<T>; } else if (vehicle.type === "motorbike") { return `This is a motorbike with an engine capacity of ${vehicle.engineCapacity} cc.` as VehicleInfo<T>; } else if (vehicle.type === "boat") { return `This is a boat with a length of ${vehicle.length} meters.` as VehicleInfo<T>; } else if (vehicle.type === "airplane") { return `This is an airplane with a wingspan of ${vehicle.wingspan} meters.` as VehicleInfo<T>; } else { return "Unknown vehicle type." as VehicleInfo<T>; } } const car: Car = { type: "car", brand: "Toyota", doors: 4, }; const motorbike: Motorbike = { type: "motorbike", brand: "Honda", engineCapacity: 150, }; const boat: Boat = { type: "boat", brand: "Yamaha", length: 10, }; const airplane: Airplane = { type: "airplane", brand: "Boeing", wingspan: 50, }; const carInfo: VehicleInfo<Car> = getVehicleInfo(car); console.log(carInfo); // Output: "This is a car with 4 doors." const motorbikeInfo: VehicleInfo<Motorbike> = getVehicleInfo(motorbike); console.log(motorbikeInfo); // Output: "This is a motorbike with an engine capacity of 150 cc." const boatInfo: VehicleInfo<Boat> = getVehicleInfo(boat); console.log(boatInfo); // Output: "This is a boat with a length of 10 meters." const airplaneInfo: VehicleInfo<Airplane> = getVehicleInfo(airplane); console.log(airplaneInfo); // Output: "This is an airplane with a wingspan of 50 meters."

Here’s the formula condition ? exprIfTrue : exprIfFalse.

extends keyword is the only operator for expressing conditions. Not ==, >=, <=, >, or <.

T extends U means T must be of U set. Is 24 a part of number? Yes. Is “I love you” a part of number? No.

If you can figure this out, you are good to go:

type A = 64 extends number ? true : false; type B = number extends 64 ? true : false; type C = string[] extends any ? true : false; type D = string[] extends any[] ? true : false; type E = never extends any ? true : false; type F = any extends any ? true : false; type G = Date extends {new (...args: any[]): any } ? true : false; type H = (typeof Date) extends {new (...args: any[]): any } ? true : false;

In the case of extending a union as a constraint, TypeScript will loop over each member of the union and return a union of its own:

type FavoriteColors = | "dark sienna" | "van dyke brown" | "yellow ochre" | [number, number, number] | { red: number; green: number; blue: number } type ColorCode<T> = T extends string ? T : never; // Is "dark sienna" assignable to string type? Yes. Append "dark sienna" // Is "van dyke brown" assignable to string type? Yes. Append "van dyke brown" // Is "yellow ochre" assignable to string type? Yes. Append "yellow ochre" // Is [number, number, number] assignable to string type? No. Ignore it. // Is { red: number; green: number; blue: number } assignable to string type? No. Ignore it. // So the ColorCode is a union of "dark sienna" | "van dyke brown" | "yellow ochre" let myColorCode: ColorCode<FavoriteColors> = "sap green";

Extract and Exclude

Extract is useful for obtaining some sub-part of a type that is assignable to some other type.

Exclude is the opposite of Extract, in that it’s useful for obtaining the part of a type that’s not assignable to some other type.

Here’s the complete source code for these types:

/** * Exclude from T those types that are assignable to U */ type Exclude<T, U> = T extends U ? never : T; /** * Extract from T those types that are assignable to U */ type Extract<T, U> = T extends U ? T : never;

Inference With Conditional Types

The infer keyword compliments conditional types and cannot be used outside an extends clause. infer allows us extract and obtain type information from larger types.

Collect the first constructor argument type:

type ConstructorArg<T> = T extends { new (arg: infer A, ...args: any[]): any } ? A : never;

Function’s first argument type:

type FirstArg<T> = T extends (firstArg: infer A, ...args: any[]) => any ? A : never;

Function’s second argument type:

type SecondArg<T> = T extends (firstArg: any, secondArg: infer A, ...args: any[]) => any ? A : never;

Function’s return type:

type ReturnTypeFunc<T> = T extends (...args: any[]) => (infer A) ? A : never;

Array’s item type:

type ArrayItem<T> = T extends (infer A)[] ? A : never;

Promise’s return type:

type PromiseReturn<T> = T extends Promise<infer A> ? A : never;

Indexed Accessed Types

Let’s say we have a nested object like this:

const person = { name: "Tatang S.", age: 42, contact: { phone: "0251322387", email: "tatang.example.org", fax: "029328392999" } };

We give a type definition to that object literal this way:

interface Person { name: string; age: number; contact: { phone: string; email: string; fax: string; } }; const person: Person = { name: "Tatang S.", age: 42, contact: { phone: "0251322387", email: "tatang.example.org", fax: "029328392999" } };

We can get the type definition of contact without authoring another type/interface by accessing the indexed type:

// This way type Contact = Person['contact']; // Instead of interface Contact { phone: string; email: string; fax: string; }

We can also use | to access multiple indexed types in Person:

type UnionOfNameAndContact = Person['name' | 'contact'];

Mapped Types

It’s a more spesific form of index signatures where you can loop over keys and specify its values based on it. It’s safe to think of Array.map as a mental model:

type MyPetKeys = 'dog' | 'cat'; type Dog = { canBark: () => void; } type Cat = { canMeow: () => void; } type Fish = { canSwim: () => void; } type Duck = { canQuack: () => void; } interface Pet { dog: Dog, cat: Cat, fish: Fish, duck: Duck }; type MyPet = { [key in MyPetKeys]: Pet[key] }; // Only Dog and Cat

With that, we can understand what’s under the hood of built-in utility Record:

// keyof any is equivalent to symbol | number | string type Record<KeyType extends keyof any, ValueType> = { [Key in KeyType]: ValueType }

And Pick:

type Pick<Type, Keys extends keyof Type> = { [Key in Keys]: Type[Key] }

We can improve MyPet above this way:

type MyPetKeys = 'dog' | 'cat'; type Dog = { canBark: () => void; } type Cat = { canMeow: () => void; } type Fish = { canSwim: () => void; } type Duck = { canQuack: () => void; } interface Pet { dog: Dog, cat: Cat, fish: Fish, duck: Duck }; type MyPet = Pick<Pet, MyPetKeys>;

Mapped types can be used to change the access type (read only, optional, required) of object properties:

/** * Make all properties in Type optional */ type Partial<Type> = { [Key in keyof Type]?: Type[Key] } /** * Make all properties in Type required */ type Required<Type> = { [Key in keyof Type]-?: Type[Key] } /** * Make all properties in Type readonly */ type Readonly<Type> = { readonly [Key in keyof Type]: Type[Key] } /** * Remove readonly in Type */ type Notreadonly<Type> = { -readonly [Key in keyof Type]: Type[Key] }

TypeScript 4.1 brought with it template literal types. It’s useful for authoring a typed class that has many methods with similar prefixes like get or set:

interface IPerson { name: string; age: number; } type Setter = { [Key in keyof IPerson as `set${Capitalize<Key>}`]: (arg: IPerson[Key]) => void } type Getter = { [Key in keyof IPerson as `get${Capitalize<Key>}`]: () => IPerson[Key] } class Person implements IPerson, Getter, Setter { constructor(public name: string, public age: number) {} getName() { return this.name; } getAge() { return this.age; } setName(name: string) { this.name = name; } setAge(age: number) { this.age = age; } }

Now we are set to go through this challenging type information:

// Get keys of type T whose values are assignable to type U type FilteredKeys<T, U> = { [P in keyof T]: T[P] extends U ? P : never }[keyof T] & keyof T

Take the first layer:

type FilterType<T, U> = { [P in keyof T]: T[P] extends U ? P : never }

We take two type parameters T and U. Map all keys of T this way:

P in keyof T

With conditional types, we make sure that T[P] is a subset of U and then extract P if condition met:

type FilterType<T, U> = { [P in keyof T]: T[P] extends U ? P : never } interface Person { name: string; age: number; greet: () => void; sleep: (time: number) => void; } type Verb = (arg: any) => any; // Is string assignable / a subset of Verb? No. Assign never // Is number assignable / a subset of Verb? No. Assign never // Is () => void assignable / a subset of Verb? Yes. Assign "greet" // Is (time: number) => void assignable / a subset of Verb? Yes. Assign "sleep" type FilteredType = FilterType<Person, Verb>; /* We will end up with: * * { * name: never; * age: never; * greet: "greet"; * sleep: "sleep"; * } */

With indexed accessed types, we can get a union of "greet" | "sleep" this way:

type FilteredType = FilterType<Person, Verb>; type FilteredKeys = FilteredType[keyof Person] & keyof Person /* We will end up with: * * "greet" | "sleep" */

& keyof Person is to make sure that the result is a subset of all properties in Person.

To make it more concise:

type FilteredKeys<T, U> = { [P in keyof T]: T[P] extends U ? P : never }[keyof T] & keyof T

Hooray!