Fun Times with Advanced TypeScript

by Nicola Marcacci Rossi

December 2018

This article is meant for people who know the basics of TypeScript but haven’t used it in a large project, and/or haven’t explored its advanced types.

My goal is to convince you, based on a ton of examples, that TypeScript’s powerful advanced type system can provide satisfying solutions for complex web engineering problems and can be used for mind-blowing and fun modeling.

Watch this video and refer to code snippets below.

Building Blocks

These are types you can find on the official TypeScript documentation. They are the building blocks for more advanced/fancy stuff. Think of this section as a cheat-sheet.

Intersection types

Get all members from multiple types:

tsx
type Duck = Quackable & Swimmable;

If you think of types as sets, what you get here is the intersection of two types, which is a smaller set. This can be confusing as the actual resulting type has a “larger” interface, i.e. the union of the members of both types.

Union types

Get common members from multiple types (the resulting type is the union between the two, i.e. a larger set):

tsx
type Flyable = Eagle | Butterfly;

Custom Type Guards

Conditional type cast:

tsx
const canFly = (animal: Animal): animal is Flyable =>
typeof (animal as any).fly === 'function';
if (canFly(animal)) {
animal.fly();
}

With the is keyword you can define functions to determine “by hand” whether a value belongs to a type, and TypeScript then assumes that type in the true part of a conditional statement.

Type Assertion

A cast shorthand if you know that a specific variable is definitely defined:

tsx
person.children[0]!.name;

Literal Types

You can use actual primitive literals (strings, booleans etc.) as types:

tsx
type Move = 'ROCK' | 'PAPER' | 'SCISSOR';

If you think of types as sets, the type 'ROCK' is the set containing only the value 'ROCK'.

Never

An expression that never resolves:

tsx
const throws = (): never => {
throw new Error('This never returns');
}
const loops = (): never => {
while (true) {
console.log('This never returns either');
}
}

This type will magically appear in many surprising spots in the examples below.

Unknown

A safer type than any for variables that you can’t trust:

tsx
let a: string;
let x: any;
a = x; // Compiles
x = a; // Compiles
let y: unknown;
y = a; // Compiles
a = y; // Does not compile

Index Types

Access the types of object keys or values:

tsx
type Duck = {
colors: string;
feathers: number;
}
type DuckProps = keyof Duck; // = 'colors' | 'feathers'
type ColorType = Duck['colors']; // = string
type DuckValues = Duck[DuckProps] // = string | number

Mapped Types

Derive an object type from another:

tsx
type Map1 = { [key: string]: string };
type Map2 = { [P in 'a' | 'b' | 'c']: string };
// = { a: string, b: string, c: string }
type Map3 = { [P in 'a' | 'b' | 'c']: P };
// = { a: 'a', b: 'b', c: 'c' }

This powerful abstraction will make more sense when you see specific applications of it.

Conditional Types

Let a type depend on another type with a condition:

tsx
type StringOrNumber<T> = T extends boolean ? string : number;
type T1 = StringOrNumber<true>; // string
type T2 = StringOrNumber<false>; // string
type T3 = StringOrNumber<Object>; // number

Here’s an actual application for conditional types:

tsx
type TypeName<T> =
T extends string ? "string":
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
type T0 = TypeName<string>; // "string"

With the infer keyword you can introduce a type variable, the type of which will be inferred:

tsx
type ElementType<T> = T extends (infer U)[] ? U : never;
type T = ElementType<[]>; // = never
type T1 = ElementType<string[]>; // = string

Misc Patterns

Functional Programming

This is not exactly related to advanced types, rather it’s an illustration of how one can program in a lightweight but type safe way with TypeScript. In Java world, in the Clean Code paradigm there are two main categories of classes: objects and data types. Data types represent data (think getters and setters), while objects represent functionality (think methods). With TypeScript one can simplify this approach even more by just using interfaces to represent data and functions to manipulate data. No bulky boilerplate, no constructors, getters and setters needed. Just return a plain object and typescript will ensure it is correctly structured.

tsx
interface RawPerson {
identifier: number;
first_name: string;
last_name: string;
}
interface Person {
id: string;
fullName: string;
}
const transformPerson = (raw: RawPerson): Person => {
return {
id: `${raw.identifier}`,
fullName: `${raw.first_name} ${raw.last_name}`,
}
}

Discriminate Unions

TypeScript understands you have exhaustively checked all possible variants of a type in a switch statement, and won’t force you to use a default statement.

tsx
type Eagle = {
kind: 'eagle';
fly: () => 'fly';
};
type Duck = {
kind: 'duck';
quack: () => 'quack';
};
type Bird = {
kind: 'bird';
};
type Animal = Eagle | Duck | Bird;
const doSomething = (animal: Animal): string => {
switch (animal.kind) {
case 'eagle':
return animal.fly();
case 'duck':
return animal.quack();
case 'bird':
return "animal.quack()";
}
}

Derive Types From Constants

Often we need literal types both as values and as types. To avoid redundancy derive the latter from the former.

tsx
const MOVES = {
ROCK: { beats: 'SCISSOR' },
PAPER: { beats: 'ROCK' },
SCISSOR: { beats: 'PAPER' },
};
type Move = keyof typeof MOVES;
const move: Move = 'ROCK';

Untrusted User Input

Untrusted user input is an application for unknown.

tsx
const validateInt = (s: unknown): number => {
let n;
switch (typeof s) {
case 'number':
// handle
case 'string':
// handle
// other cases
default:
throw new Error('Not a number.');
}
}

Mapped Types

Create a readonly version of a type:

tsx
type Readonly<T> = { readonly [P in keyof T]: T[P] };
type ReadonlyDuck = Readonly<Duck>;
// = { readonly color: string; readonly feathers: number }

Derive a partial version of a type:

tsx
type Partial<T> = { [P in keyof T]?: T[P] };
type PartialDuck = Partial<Duck>;
// = { color?: string; feathers?: number }

Derive a version of a type where all fields are required:

tsx
type PartialDuck = {
color?: string;
feathers?: number;
}
type Required<T> = { [P in keyof T]-?: T[P] };
type Duck = Required<PartialDuck>;
// = { color: string; feathers: number }

Derive a nullable version of a type:

tsx
type Nullable<T> = { [P in keyof T]: T[P] | null }
type NullableDuck = Partial<Duck>;
// = { color: string | null; feathers: number | null }

Derive a type with only specific fields (Pick):

tsx
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type ColorDuck = Pick<Duck, 'color'>;

An example from practice, where we select specific columns from a SQL table, and TypeScript automatically derives the partial type for us:

tsx
async function fetchPersonById<T extends keyof Person>(
id: string,
...fields: T[]
): Promise<Pick<Reaction, T>> {
return await knex('Person')
.where({ id })
.select(fields)
.first();
}
const reaction = await fetchPersonById(id, 'name', 'age');
// = { name: string, age: number }

Derive a record (all properties have same given type) from a type:

tsx
type Record<K extends string, T> = { [P in K]: T };
type Day = 'Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday' | 'Saturday' | 'Sunday';
type EverydayParty = Record<Day, 'party'>;
/*
= {
Monday: 'party';
Tuesday: 'party';
Wednesday: 'party';
Thursday: 'party';
Friday: 'party';
Saturday: 'party';
Sunday: 'party';
}
*/

Conditional Types

Filter union types

Filter / extract a subset from a type:

tsx
type Filter<T, U> = T extends U ? T : never;
type T1 = Filter<"a" | "b", "a">; // = "a"
// Natively available as Extract
type T2 = Extract<"a" | "b", "a"> // = "a"

Diff / exclude a subset from a type:

tsx
type Diff<T, U> = T extends U ? never : T;
type T1 = Diff<"a" | "b", "a">; // = "b"
// Natively available as Exclude
type T2 = Exclude<"a" | "b", "a"> // = "b"

Exclude null and undefined from a typed (thus making it non-nullable):

tsx
type NonNullable<T> = Diff<T, null | undefined>;
type T = NonNullable<string | null | undefined>; // = string

Omit specific fields from a type:

tsx
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
type PartialDuck = Omit<Duck, 'feathers'>;
// = { color: string }

Here is an example from practice (how to type a React.js HOC correctly):

tsx
export interface WithUserProps {
user: User;
}
export const withUser = <P extends WithUserProps>
(Component: React.ComponentType<P>) => (props: Omit<P, WithUserProps>) => (
<Component {...props} user={getUser()} />
)
const UsernameComponent = ({ user, message }: { user: User, message: string }) => (
<div>Hi {user.username}! {message}</div>
)
const Username = withUser(UsernameComponent); // ({ message }) => JSX.Element

Combining with mapped types

Get the function properties of an object:

tsx
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never
}[keyof T];
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;
type Duck = {
color: string,
fly: () => void,
}
type T1 = FunctionProperties<Duck>; // = { fly: () => void }
type T2 = FunctionPropertyNames<Duck>; // = "fly"

Get the non-function properties of an object:

tsx
type NonFunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? never : K
}[keyof T];
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;
type Duck = {
color: string,
fly: () => void,
}
type T1 = NonFunctionProperties<Duck>; // = { color: string }
type T2 = NonFunctionPropertyNames<Duck>; // = "color"

Get the return type of a function:

tsx
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
const x = (a: string): number => 1;
type R = ReturnType<typeof x>; // = number

Get the instance type of a constructor function:

tsx
type InstanceType<T extends new (...args: any[]) => any>
= T extends new (...args: any[]) => infer R ? R : any;
class Duck {}
const make2 = <T extends new (...args: any[]) => any>
(constructor: T): [InstanceType<T>, InstanceType<T>] =>
[new constructor(), new constructor()]
const ducks = make2(Duck); // = [Duck, Duck]

Promisifying Redis

Another example from practice. All methods in the npm redis library use callbacks. We like to use promises and async/await. We can derive the new type from the old based on how we build it.

tsx
import { ClientOpts, createClient } from 'redis';
import { promisify } from 'util';
export const promisifyRedis = (opts: ClientOpts) => {
const redis = createClient(opts);
const promisifiedRedis = {
setex: promisify(redis.setex),
get: promisify(redis.get),
};
const wrappedRedis: typeof promisifiedRedis = {} as any;
for (const key of Object.keys(promisifiedRedis) as (keyof typeof promisifiedRedis)[]) {
wrappedRedis[key] = promisifiedRedis[key].bind(redis);
}
return wrappedRedis;
};
export type Redis = ReturnType<typeof promisifyRedis>;
/* =
type Redis = {
setex: (arg1: string, arg2: number, arg3: string) => Promise<string>;
get: (arg1: string) => Promise<...>;
}
*/

Conclusion

I hope to have convinced you of the power of TypeScript. Now go ahead and TYPE!

Resources

Previous post
Back to overview
Next post