Advanced TypeScript

Get familiar with some of Typescript's greatest advanced features.

emoji_objects emoji_objects emoji_objects
Kevin Kreuzer

Kevin Kreuzer

@kreuzercode

Nov 10, 2022

7 min read

Advanced TypeScript
share

Typescript is awesome. It offers so many great features. Hereโ€™s a summary of some of the greatest advanced Typescript features.

  • Union and intersection types
  • Keyof
  • Typeof
  • Conditional types
  • Utility types
  • Infer type
  • Mapped types

By the end of this blog post, you should have a base understanding of each of those operators and you should be able to use them in your projects.

Union and intersection types

Typescript allows us to combine multiple types to create a new type. This approach is similar to logical expressions in JavaScript where we can use the logical OR || or the logical AND && to create new powerful checks.

Union types

A union type is similar to Javascripts OR expression. It allows you to use two or more types (union members) to form a new type that may be any of those types.

function orderProduct(orderId: string | number) {
  console.log('Ordering product with id', orderId);
}

// ๐Ÿ‘
orderProduct(1);

// ๐Ÿ‘
orderProduct('123-abc');

// ๐Ÿ‘Ž Argument is not assignable to string | number
orderProduct({ name: 'foo' });

We type the orderProduct method with a union type. TypeScript will throw an error once we call the orderProduct method with anything that is not a number or a string.

Intersection types

An intersection type, on the other hand, combines multiple types into one. This new type has all the features of the combined types.

interface Person {
  name: string;
  firstname: string;
}

interface FootballPlayer {
  club: string;
}

function tranferPlayer(player: Person & FootballPlayer) {}

// ๐Ÿ‘
transferPlayer({
  name: 'Ramos',
  firstname: 'Sergio',
  club: 'PSG',
});

// ๐Ÿ‘Ž Argument is not assignable to Person & FootballPlayer
transferPlayer({
  name: 'Ramos',
  firstname: 'Sergio',
});

The transferPlayer method accepts a type that contains all features of both Person and FootballPlayer. Only an object containing the name, firstname and the club property is valid.

Keyof

Now that we know the union type. Letโ€™s have a look at the keyof operator. The keyof operator takes the keys of an interface or an object and produces a union type.

interface MovieCharacter {
  firstname: string;
  name: string;
  movie: string;
}

type characterProps = keyof MovieCharacter;

Got it! But when is this useful. We could also type the characterProps out.

type characterProps = 'firstname' | 'name' | 'movie';

Yes, we could. keyof makes our code more robust and always keeps our types up to date. Letโ€™s explore this with the following example.

interface PizzaMenu {
  starter: string;
  pizza: string;
  beverage: string;
  dessert: string;
}

const simpleMenu: PizzaMenu = {
  starter: 'Salad',
  pizza: 'Pepperoni',
  beverage: 'Coke',
  dessert: 'Vanilla ice cream',
};

function adjustMenu(
  menu: PizzaMenu,
  menuEntry: keyof PizzaMenu,
  change: string,
) {
  menu[menuEntry] = change;
}

// ๐Ÿ‘
adjustMenu(simpleMenu, 'pizza', 'Hawaii');
// ๐Ÿ‘
adjustMenu(simpleMenu, 'beverage', 'Beer');

// ๐Ÿ‘Ž Type - 'bevereger' is not assignable
adjustMenu(simpleMenu, 'bevereger', 'Beer');
// ๐Ÿ‘Ž Wrong property - 'coffee' is not assignable
adjustMenu(simpleMenu, 'coffee', 'Beer');

The adjustMenu function allows you to change a menu. For example, imagine you like the menuSimple but you prefer to drink beer over a Coke. In this case, we call the adjustMenu function with the menu, the menuEntry and the change, in our case, a Beer.

The interesting part of this function is that the menuEntry is typed with the keyof operator. The nice thing here is that our code is very robust. If we refactor the PizzaMenu interface, we donโ€™t have to touch the adjustMenu function. It is always up to date with the keys of the PizzaMenu.

Follow me on Twitter because you will get notified about new TypeScript blog posts and cool frontend stuff!๐Ÿ˜‰

Typeof

typeof allows you to extract a type from a value. It can be used in a type context to refer to the type of a variable.

let firstname = 'Frodo';
let name: typeof firstname;

Of course, this doesnโ€™t make much sense in such simple scenarios. But let's look at a more sophisticated example. In this example, we use typeof in combination with ReturnType to extract typing information from a functions return type.

function getCharacter() {
  return {
    firstname: 'Frodo',
    name: 'Baggins',
  };
}

type Character = ReturnType<typeof getCharacter>;

/*

equal to 

type Character = {
  firstname: string;
  name: string;
}

*/

In the example above, we create a new type based on the return type of the getCharacter function. Same here, if we refactor the return type of this function changes, our Character type is up to date.

Conditional types

The conditional ternary operator is a very well-known operator in Javascript. The ternary operator takes three operands. A condition, a return type if the condition is true, and a return type is false.

condition ? returnTypeIfTrue : returnTypeIfFalse;

The same concept also exists in TypeScript.

interface StringId {
  id: string;
}

interface NumberId {
  id: number;
}

type Id<T> = T extends string ? StringId : NumberId;

let idOne: Id<string>;
// equal to let idOne: StringId;

let idTwo: Id<number>;
// equal to let idTwo: NumberId;

In this example, we use the Id type util to generate a type based on a string. If T extends string we return the StringId type. If we pass a number, we return the NumberId type.

Utility types

Utility types are helper tools to facilitate common type transformations. Typescript offers many utility types. Too many to cover in this blog post. Below you can find a selection of the ones I use the most.

The official TypeScript documentation offers a great list of all utility types.

Partial

The Partial utility type allows you to transform an interface into a new interface where all properties are optional.

interface MovieCharacter {
  firstname: string;
  name: string;
  movie: string;
}

function registerCharacter(character: Partial<MovieCharacter>) {}

// ๐Ÿ‘
registerCharacter({
  firstname: 'Frodo',
});

// ๐Ÿ‘
registerCharacter({
  firstname: 'Frodo',
  name: 'Baggins',
});

MovieCharacter requires a firstname, name and a movie. However, the signature of the registerPerson function uses the Partial utility to create a new type with optional firstname, optional name and optional movie.

Required

Required does the opposite of Partial. It takes an existing interface with optional properties and transforms it into a type where all properties are required.

interface MovieCharacter {
  firstname?: string;
  name?: string;
  movie?: string;
}

function hireActor(character: Required<MovieCharacter>) {}

// ๐Ÿ‘
hireActor({
  firstname: 'Frodo',
  name: 'Baggins',
  movie: 'The Lord of the Rings',
});

// ๐Ÿ‘Ž
hireActor({
  firstname: 'Frodo',
  name: 'Baggins',
});

In this example the properties of MovieCharacter were optional. By using Required we transformed into a type where all properties are required. Therefore only objects containing the firstname, name and movie properties are allowed.

Extract

Extract allows you to extract typing information from a type. Extract accepts two Parameters, first the Interface and second the type that should be extracted.

type MovieCharacters =
  | 'Harry Potter'
  | 'Tom Riddle'
  | { firstname: string; name: string };

type hpCharacters = Extract<MovieCharacters, string>;
// equal to type hpCharacters = 'Harry Potter' | 'Tom Riddle';

type hpCharacters = Extract<MovieCharacters, { firstname: string }>;
// equal to type hpCharacters = {firstname: string; name: string };

Extract<MovieCharacters, string> creates a union type hpCharacters that consists only of strings. Extract<MovieCharacters, {firstname: string}> on the other hand, it extracts all object types that contain a firstname: string type.

Exclude

Exclude does the opposite of extract. It allows you to generate a new type by excluding a type.

type MovieCharacters =
  | 'Harry Potter'
  | 'Tom Riddle'
  | { firstname: string; name: string };

type hpCharacters = Exclude<MovieCharacters, string>;
// equal to type hpCharacters = {firstname: string; name: string };

type hpCharacters = Exclude<MovieCharacters, { firstname: string }>;
// equal to type hpCharacters = 'Harry Potter' | 'Tom Riddle';

First, we generate a new type that excludes all strings. Next, we generate a type that excludes all object types containing firstname: string.

Infer type

infer allows you to create a new type. It's similar to creating a variable in Javascript with the keyword var, let or const.

type flattenArrayType<T> = T extends Array<infer ArrayType> ? ArrayType : T;

type foo = flattenArrayType<string[]>;
// equal to type foo = string;

type foo = flattenArrayType<number[]>;
// equal to type foo = number;

type foo = flattenArrayType<number>;
// equal to type foo = number;

Wow, the getArrayType looks pretty complicated. But actually, itโ€™s not. Letโ€™s go through it.

T extends Array<infer ArrayType> checks if T extends an Array. Furthermore, we use the infer keyword to get a hold of the array type. Think of it as storing the type in a variable.

We then use the conditional type to return the ArrayType if T extends Array. If not, we return T.

Mapped types

Mapped types are a great way of transforming existing types into new types. Therefore the term map. Mapped types are compelling and allow us to create custom utility types.

interface Character {
  playInFantasyMovie: () => void;
  playInActionMovie: () => void;
}

type toFlags<Type> = { [Property in keyof Type]: boolean };

type characterFeatures = toFlags<Character>;

/*

equal to 

type characterFeatures = {
  playInFantasyMovie: boolean;
  playInActionMovie: boolean;
}

*/

We create the toFlags helper type that takes a type and maps all properties to be of return type boolean.

Pretty cool. But it gets even more powerful. We can add or remove the ? or the readonly modifier by prefixing them with a simple + or -.

Letโ€™s have a look at an example where we create a mutable utility type.

type mutable<Type> = {
  -readonly [Property in keyof Type]: Type[Property];
};

type Character = {
  readonly firstname: string;
  readonly name: string;
};

type mutableCharacter = mutable<Character>;
/*

equal to

type mutableCharacter = {
  firstname: string;
  name: string;
}

 */

Each property of the Character type is readonly. Our mutable interface removes the readonly property because we prefix it with a -.`

The same works in the other direction. If we add a + we can create a helper type that takes an interface and transforms it into an interface where every property is optional.

type optional<Type> = {
  [Property in keyof Type]+?: Type[Property];
};

type Character = {
  firstname: string;
  name: string;
};

type mutableCharacter = optional<Character>;

/* 

equal to

type mutableCharacter = {
  firstname?: string;
  name?: string;
}

*/

Of course, those two approaches can also be combined. Look at the next example where the optionalAndMutable type removes the readonly property and adds a ? which makes each property optional.

type optionalAndMutable<Type> = {
  -readonly [Property in keyof Type]+?: Type[Property];
};

type Character = {
  readonly firstname: string;
  readonly name: string;
};

type mutableCharacter = optionalAndMutable<Character>;

/*

equal to

type mutableCharacter = {
  firstname?: string;
  name?: string;
}

 */

It even gets more powerful. Letโ€™s check out the following example where we create type util that transforms an existing type into a type of setters.

type setters<Type> = {
  [Property in keyof Type as `set${Capitalize<
    string & Property
  >}`]: () => Type[Property];
};

type Character = {
  firstname: string;
  name: string;
};

type character = setters<Character>;

/*

equal to

type character = {
  setFirstname: () => string;
  setName: () => string;
}

*/

There are no limitations. We can even reuse everything we saw so far. How about a mapped type that uses the Exclude utility type?

type nameOnly<Type> = {
  [Property in keyof Type as Exclude<Property, 'firstname'>]: Type[Property];
};

type Character = {
  firstname: string;
  name: string;
};

type character = nameOnly<Character>;

/*

equal to 

type character = {
  name: string;
}

*/

Thatโ€™s it. TypeScript is awesome, and it offers even more features. Once mastered the concepts described in this article are very powerful and can make your code more robust and therefore easier to refactor.

Do you enjoy the theme of the code preview? Explore our brand new theme plugin

Skol - the ultimate IDE theme

Skol - the ultimate IDE theme

Northern lights feeling straight to your IDE. A simple but powerful dark theme that looks great and relaxes your eyes.

Do you enjoy the content and would like to learn more about how to ensure long term maintainability of your Angular application?

Angular Enterprise Architecture Ebook

Angular Enterprise Architecture Ebook

Learn how to architect a new or existing enterprise grade Angular application with a bulletproof tooling based automated architecture validation.

This will ensure that Your project stays maintainable, extendable and therefore with high delivery velocity over the whole project lifetime!

Get notified
about new blog posts

Sign up for Angular Experts Content Updates & News and you'll get notified whenever we release a new blog posts about Angular, Ngrx, RxJs or other interesting Frontend topics!

We will never share your email with anyone else and you can unsubscribe at any time!

Emails may include additional promotional content, for more details see our Privacy policy.
Kevin Kreuzer - GDE for Angular & Web Technologies

Kevin Kreuzer

Google Developer Expert (GDE)
for Angular & Web Technologies

A trainer, consultant, and senior front-end engineer with a focus on the modern web, as well as a Google Developer Expert for Angular & Web technologies. He is deeply experienced in implementing, maintaining and improving applications and core libraries on behalf of big enterprises worldwide.

Kevin is forever expanding and sharing his knowledge. He maintains and contributes to multiple open-source projects and teaches modern web technologies on stage, in workshops, podcasts, videos and articles. He is a writer for various tech publications and was 2019โ€™s most active Angular In-Depth publication writer.

58

Blog posts

2M

Blog views

23

NPM packages

3M+

Downloaded packages

39

Videos

14

Celebrated Champions League titles

Responses & comments

Do not hesitate to ask questions and share your own experience and perspective with the topic

You might also like

Check out following blog posts from Angular Experts to learn even more about related topics like TypeScript !

Typescript 5 decorators

Typescript 5 decorators

TypeScript five has just been released. In this release, TypeScript has implemented the new upcoming ECMA script decorators standard. Letโ€™s take a look.

emoji_objects emoji_objects emoji_objects
Kevin Kreuzer

Kevin Kreuzer

@kreuzercode

Apr 3, 2023

6 min read

Angular & tRPC

Angular & tRPC

Maximum type safety across the entire stack. How to setup a fullstack app with Angular and tRPC.

emoji_objects emoji_objects emoji_objects
Kevin Kreuzer

Kevin Kreuzer

@kreuzercode

Jan 24, 2023

6 min read

Angular Signal Inputs

Angular Signal Inputs

Revolutionize Your Angular Components with the brand new Reactive Signal Inputs.

emoji_objects emoji_objects emoji_objects
Kevin Kreuzer

Kevin Kreuzer

@kreuzercode

Jan 24, 2024

6 min read

Empower your team with our extensive experience

Angular Experts have spent many years consulting with enterprises and startups alike, leading workshops and tutorials, and maintaining rich open source resources. We take great pride in our experience in modern front-end and would be thrilled to help your business boom

or