TypeScript can sometimes feel like bunch of nonsense. But once you internalize a few key patterns, the type system starts to feel like a powerful tool instead of an obstacle.
Here are a few tricks that I’ve personally found valuable, especially if you’re working on a large codebase or building libraries.
1. Optional Keys vs Optional Values
There’s a big difference between marking a property as optional and allowing undefined
as a value. These look similar:
But they behave differently:
foo?: string
means the keyfoo
might not be present at all.foo: string | undefined
meansfoo
is always present, but its value might beundefined
.
Why this matters? The difference between them affects the way you use it:
- How you check if a property exists
- Function argument inference
- Serializing/deserializing data from external sources
Example
As a rule of thumb, use foo?: Type
(Optional Key) when the property might not be present, and foo: Type | undefined
(Optional Value) when the key is always expected.
2. Pick and Omit for Object Key Manipulation
When you need to reshape object types by including or excluding specific keys, Pick
and Omit
are your go-to tools.
This type utilities are part of the language and are always available for your use, they are particulary useful when you need to derive types.
Don't forget, Pick and Omit works over Object shapes
Pick
Pick
is useful when exposing a public-facing API without leaking sensitive fields, it lets you choose what properties will be included
Omit
Use Omit
when you want to reuse a base type but remove some fields for specific scenarios, like forms or UI display logic.
These utilities keep your types DRY and consistent. Instead of redefining structures, you can derive them from existing ones, reducing duplication and potential errors.
3. Extract and Exclude for Union Filtering
Unlike Pick
and Omit
, which work on object keys, Extract
and Exclude
operate on union types, they are pretty similar in how the work.
Extract
Use Extract
when you want to narrow down a union to only include specific members.
Exclude
Use Exclude
when you want to remove one or more members from a union.
These are essential tools when refining API responses, filtering variants, or creating conditional logic that needs to type-check specific string literal options.
4. Loose Autocomplete
This is a different kind of trick, this one affects how your editor understand your types.
Sometimes you want to offer a set of suggestions to the user without forcing them to pick from a fixed list.
The trick
Now TypeScript will offer autocompletion for the variants defined in the Variant
union, but will also allow allow any other string.
string & {}
is essentially still a string, but TypeScript treats it as a branded version, preserving autocomplete while keeping it assignable to string
.
Here you can found another article related to Branded Types
Use cases
- Props with suggested values
- Config objects
- Third-party data with flexible inputs
5. Mapped Types for Smart Rewrites
Mapped types let you create new types from existing ones by transforming each property. This concept is a cornerstone of TypeScript’s type system and is covered more deeply in my article: Learn the Key Concepts of TypeScript’s Powerful Generic and Mapped Types.
You can consider Mapped types as some kind of function that transform a type in another by using all of the utilities and features of Typescript, like the above utilities and tricks, but also string literals, and more.
Example: feature flags
General Utility Type
Mapped types are perfect for:
- Adapting backend responses
- Creating flexible utility types
- Rewriting shapes while preserving keys
- Enforcing contracts between layers
They’re also essential in combination with generics to create reusable type logic across your app or library.
Found more about Generics in the following article An Introduction to Typescript Generics
6. The IIMT Pattern (Infer Inside Mapped Type)
This pattern unlocks the power of infer
within mapped types, allowing you to extract and reuse internal type information from complex structures.
It dynamically inspects each property of a type (by using infer
), and if that property matches a specific shape (e.g., a function), it extracts part of it.
This means you can build new types that are entirely based on the structure of other types without manually copying anything.
The pattern
Here, ReturnTypes iterates over each key in the API object and uses conditional types to check if the value is a function. If it is, it uses infer R to grab the return type of that function.
How it works
keyof T
iterates over each key of the input type.T[K]
accesses the value type at each key.T[K] extends (...args: any) => infer R
checks if that value is a function. (consider this as a conditional block)infer R
captures the return type of the function.- If the value is not a function, the fallback is never.
This pattern is composable—you can use it to build return type maps, argument type maps, or anything else you need.
This will give you a type with the argument list of each function in a mapped type.
You can use this in different situations like:
- Generating automatic schemas
- Building strongly-typed service clients
- Rewriting function maps
- Validating types without duplicating logic
If you’re building libraries or dynamic APIs, this pattern is a must-have in your TypeScript toolbox.
Once it clicks, it opens the door to expressive, scalable, and maintainable type logic.