Delve into some of TypeScript's most powerful advanced capabilities.
Kevin Kreuzer
@nivekcode
Nov 10, 2022
7 min read
TypeScript is remarkable. It provides an extensive set of powerful features. This article highlights a few of the most useful advanced TypeScript capabilities.
- Union and intersection types
- Keyof
- Typeof
- Conditional types
- Utility types
- Infer type
- Mapped types
Once you finish reading this post, you will have a solid grasp of each operator and will be ready to apply them in your own code.
Union and intersection types
With TypeScript, you can merge multiple types into a single one. This works much like logical operations in JavaScript, where the OR operator || or the AND operator && enable you to construct more complex checks.
Union types
Think of a union type as the type-level counterpart to JavaScript's OR expression. It lets you combine two or more types—called union members—into a new type that can be any of the constituent 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 assign a union type to the orderProduct method. As soon as we invoke orderProduct with a value that
falls outside the number or string range, TypeScript raises an error.
Intersection types
Conversely, an intersection type merges several types into a single type. The resulting type possesses every capability
of the types that were combined.
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',
});
When transferPlayer is called, its argument must cover every property from both Person and FootballPlayer. In practice, only an object that supplies the name, firstname, and club fields will pass the type check.
Keyof
With union types out of the way, it's time to explore the keyof operator. This operator gathers all keys defined on a given interface or object and emits them as a union type.
interface MovieCharacter {
firstname: string;
name: string;
movie: string;
}
type characterProps = keyof MovieCharacter;
You may be wondering about the practical value of this approach, especially since we could simply write the characterProps type explicitly.
type characterProps = 'firstname' | 'name' | 'movie';
That approach is viable. By relying on keyof, we harden the implementation and ensure our typings stay synchronized without manual fixes. Consider this demonstration to see it in action.
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');
With the adjustMenu function, you can modify a menu item. Say you’re happy with menuSimple but want to swap a Coke for beer. In that scenario, you’d invoke adjustMenu, passing in the menu, the menuEntry, and the change—in this case, a Beer.
What stands out here is how menuEntry is defined using the keyof operator. This makes the code remarkably resilient. If you ever refactor the PizzaMenu interface, the adjustMenu function stays in sync automatically—no manual updates are ever needed to match the keys of PizzaMenu.
Follow me on Twitter because you will get notified about new TypeScript blog posts and cool frontend stuff!😉
Typeof
The typeof operator lets you derive a type from an existing value. In a type context, it can be applied to reference the type that a variable has.
let firstname = 'Frodo';
let name: typeof firstname;
Naturally, in trivial cases like these, this approach offers little benefit. However, a more complex illustration is worth examining. Here, we combine typeof with ReturnType to pull type details out of a function’s returned value.
function getCharacter() {
return {
firstname: 'Frodo',
name: 'Baggins',
};
}
type Character = ReturnType<typeof getCharacter>;
/*
equal to
type Character = {
firstname: string;
name: string;
}
*/
The new Character type above is derived directly from whatever getCharacter returns. So if we later change that function’s return type during a refactor, this derived type stays in sync automatically.
Conditional types
JavaScript developers are quite familiar with the ternary conditional operator. This operator works with three operands: one for the condition, one for the result when the condition holds true, and one for the result when it does not.
condition ? returnTypeIfTrue : returnTypeIfFalse;
TypeScript applies the very same principle.
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;
Here, the Id utility type is applied to craft a type derived from a string. When T qualifies as a string, the resulting type is StringId. Conversely, supplying a number yields the NumberId type.
Utility types
Utility types serve as aids for standard type modifications. The TypeScript ecosystem includes numerous utility types, far more than can be discussed here. The following list highlights a few that I frequently rely on.
A comprehensive reference for all utility types is available in the official TypeScript documentation.
Partial
The Partial utility type converts an interface into another interface where every property becomes non-mandatory.
interface MovieCharacter {
firstname: string;
name: string;
movie: string;
}
function registerCharacter(character: Partial<MovieCharacter>) {}
// 👍
registerCharacter({
firstname: 'Frodo',
});
// 👍
registerCharacter({
firstname: 'Frodo',
name: 'Baggins',
});
The MovieCharacter interface expects a firstname, a name, and a movie. But the registerPerson function's signature leverages the Partial utility type, producing a new type where firstname, name, and movie are each optional.
Required
Required inverts the behavior of Partial. Given an interface whose properties are all optional, Required converts it into a type that mandates every property.
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',
});
Here, every property on MovieCharacter was marked optional. Applying Required converts the type so that each property must be present. Consequently, the only accepted objects are those that include the firstname, name, and movie fields.
Extract
The Extract utility enables you to pull type information out of an existing type. It takes two arguments: the interface to pull from first, and the specific type to target second.
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> yields a union type hpCharacters containing exclusively strings. Conversely, Extract<MovieCharacters, {firstname: string}> pulls out every object type whose shape includes a firstname: string property.
Exclude
Exclude performs the inverse operation of extract. With it, you can construct a fresh type by removing a given type from the original set.
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';
To begin with, we create a fresh type that strips out every string. After that, we build another type that filters out any object shapes featuring firstname: string.
Infer type
With infer, you can construct a brand-new type. This works much like declaring a variable in JavaScript using 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;
At first glance, getArrayType may seem quite intricate. In reality, though, it’s not that complex. Let’s break it down.
With T extends Array<infer ArrayType>, we verify whether T is an Array. Along the way, the infer keyword lets us capture the array’s element type. Essentially, you can treat this as saving that type into a variable.
Next, via a conditional type, we return the captured ArrayType when T is indeed an Array. In any other case, we simply output T.
Mapped types
To transform existing types into new ones, mapped types serve as an excellent tool—hence the "map" designation. These have real power, opening the door to crafting 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 define a toFlags helper type that receives a type and converts every one of its properties into a boolean return type.
That’s neat, but the real power lies ahead. By adding a + or - sign in front of the ? or readonly modifier, we can either apply or strip it away.
Take a look at this example that builds 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;
}
*/
The Character type has every field marked as readonly. By applying a leading -, our mutable interface strips away the readonly modifier from each property.
This approach also functions in reverse. Adding a + in front of the modifier yields a utility type that accepts an interface and outputs a new interface with all properties set as 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;
}
*/
Naturally, these two strategies can be used together as well. In the following example, the optionalAndMutable type drops the readonly modifier and appends a ? to render every 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;
}
*/
The story doesn’t stop here. Look at this next case, where a helper type is built to convert an existing type into a corresponding set of setter types.
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;
}
*/
Nothing is off the table here. Every concept covered up to this point can be reused freely. What if we build a mapped type that puts the Exclude utility type to work?
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 covers it. TypeScript is truly impressive, and there’s much more it can do. Once you’re comfortable with the ideas presented here, they’ll prove highly effective—sharpening your code’s reliability and simplifying future maintenance.
Liking what you see in the code sample? Check out our brand-new theme plugin
Skol - the ultimate IDE theme
Bring the aurora borealis experience directly into your editor. This dark theme is both minimal and effective, offering great visuals while keeping eye strain low.
Create more intelligent interfaces by combining Angular with AI
Video Training on Angular and Artificial Intelligence
This practical workshop walks through integrating AI into Angular applications with Hash Brown, enabling intelligent and responsive interfaces.
Explore streaming chat, tool invocation, generative UI, structured outputs, and similar patterns incrementally.
If you find the material useful and seek deeper insights into keeping your Angular project maintainable over time, read on below.
Angular Enterprise Architecture eBook
Discover how to structure a greenfield or legacy enterprise-grade Angular app through automation-based architecture validation that is rock solid.
This approach guarantees that your codebase remains clear, scalable and, as a result, delivers rapid development speed throughout the entire life cycle of the application!
Are you enjoying this material and want to dive into the brand new Signal Forms in Angular?
Signal Forms in Angular: A Practical Deep Dive
Angular's latest Signal-Forms are covered end-to-end across 12 incremental chapters, blending theoretical insights with practical exercises.
Dive into core form handling, validation rules, bespoke controls, nested forms, and proven migration approaches—plus additional topics.
Stay in the loop
with our latest articles
Subscribe to Angular Experts Content Updates & News and receive a notification with every fresh post we publish on Angular, Ngrx, RxJs, and other fascinating Frontend subjects.
Your email is never shared with anyone, and you are free to unsubscribe anytime!
Your thoughts & feedback
Feel free to ask anything and contribute your own insights and perspectives on the subject
You might also like
Explore other posts from the Angular Experts team to dive deeper into similar subjects, with a special focus on TypeScript !

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.

Kevin Kreuzer
@nivekcode
Apr 3, 2023
6 min read

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

Kevin Kreuzer
@nivekcode
Jan 24, 2023
6 min read

Angular Signal Forms: Custom Controls Without ControlValueAccessor
Build reusable Angular custom controls with FormValueControl, model(), touch events, and schema-driven validation—without writing a ControlValueAccessor.

Kevin Kreuzer
@nivekcode
Aug 12, 2026
7 min read
Put our deep expertise to work for your team
Our consultants at Angular Experts have partnered with enterprises and startups for years, guiding workshops, crafting tutorials, and curating valuable open source projects. Our wealth of modern front-end knowledge is something we are proud of, and we look forward to seeing your business thrive with our help
