In the dynamic landscape of TypeScript development, utility types stand as base tools for crafting adaptable, clear, and robust type arrangements. This article introduces 10 widely-used utility types that tackle common coding challenges, from manipulating primitive types to fine-tuning object properties for comprehensive control over immutability and optionality.
Second part: 11-20 Сustom Utility Types for TypeScript Projects
TOC
- Primitive
- Falsy
- Truthy
- Nullish
- NonNullableKeys
- JSONObject
- OptionalExceptFor
- ReadonlyDeep
- PartialDeep
- Brand
Primitive
The type Primitive
represents the set of all basic data types in JavaScript (TypeScript). Primitive can be useful for functions or variables that may need to handle a range of simple data types.
type Primitive = string | number | bigint | boolean | symbol |
null | undefined;
Example
The following example demonstrates how filters can accommodate various primitive data types effectively.
interface Product {
id: symbol; // Unique identifier
name: string;
price: number;
available: boolean;
totalSales: bigint; // Large number representing total sales
}
// The filter definition
interface FilterDefinition {
key: keyof Product;
value: Primitive;
}
// Function to filter products
function filterProducts(
products: Product[],
filters: FilterDefinition[]
): Product[] {
return products.filter((product) => {
return filters.every((filter) => {
return product[filter.key] === filter.value;
});
});
}
Falsy
The type Falsy
encompasses all possible values that JavaScript (TypeScript) considers "falsy". In JavaScript, a value is considered falsy if it translates to false
when evaluated in a boolean context (e.g., in an if
statement). This type is optimally designed for scenarios involving type coercion to Boolean across different primitive types.
type Falsy = false | "" | 0 | 0n | null | undefined;
Example
For instance, consider a form field that may accept several falsy values, including null
, false
, 0
, and empty string.
// A utility function that returns a default value if the
// input is Falsy
function getDefaultIfFalsy<T>(
value: T | Falsy,
defaultValue: T
): T {
return value || defaultValue;
}
// Define form data interface
interface FormData {
name: string;
email: string;
age: number;
billingAddress: string;
shippingAddress?: string;
sameAsBillingAddress: boolean;
}
// Use `getDefaultIfFalsy` for `shippingAddress`
formData.shippingAddress =
getDefaultIfFalsy(formData.shippingAddress, "");
Truthy
This construction allows Truthy<T>
to be used to filter out falsy values from type unions, preserving only those types that are considered truthy in JavaScript.
type Truthy<T> = T extends Falsy ? never : T;
Here's how it can work in practice:
// Result: 1 | {}
type Example = Truthy<"" | 1 | false | {} | undefined>;
Example
Utilizing the Truthy
type, you can create a function that takes an object with optional properties as input and returns an object with only the properties that were filled out (truthy values), fully typed.
function processInput<T extends object>(
obj: T
): {[K in keyof T]: Truthy<T[K]>} {
const result: Partial<{[K in keyof T]: Truthy<T[K]>}> = {};
Object.entries(obj).forEach(([key, value]) => {
if (value) {
result[key as keyof T] = value as any;
}
});
return result as {[K in keyof T]: Truthy<T[K]>};
}
const userInput = {
name: "John",
age: 0,
email: ""
};
const processedInput = processInput(userInput);
console.log(processedInput); // Output: { name: "John" }
Nullish
The Nullish
type indicates the absence of a value or signifies that a variable has not been initialized. Its primary purpose is to handle optional properties, variables or function parameters that may not always have a value. It allows to distinguish between missing values and values that are present, but with falsy values like 0
, false
, or an empty string. Therefore, using Nullish
helps enhance the reliability of the code by explicitly handling these null
or undefined
cases.
type Nullish = null | undefined;
Example
In the example, Nullish
is used to manage optional UserInfo
properties, allowing the function getFoodRecommendations
to default to specific values when those properties are not provided. This approach ensures the function can handle cases where the properties are either null
or undefined
, preventing potential errors that could arise from directly accessing unset or optional properties.
interface UserInfo {
name: string;
favoriteFood?: string | Nullish;
dietRestrictions?: string | Nullish;
}
function getFoodRecommendations(user: UserInfo) {
const favoriteFood = user.favoriteFood ?? 'Generic';
const dietRestriction = user.dietRestrictions ?? 'No Special Diet';
// In a real app, there could be a complex logic to get proper food recommendations.
// Here, just return a simple string for demonstration.
return `Recommendations: ${favoriteFood}, ${dietRestriction}`;
}
NonNullableKeys
The NonNullableKeys
type construction is used to filter out the keys of an object type that are associated with nullable (i.e., null
or undefined
) values. This utility type is particularly useful in scenarios where it's necessary to ensure the access of only those properties of an object that are guaranteed to be non-null
and non-undefined
. It can be applied, for example, in functions that require strict type safety and cannot operate on nullable properties without explicit checks.
type NonNullableKeys<T> = {
[K in keyof T]: T[K] extends Nullish ? never : K
}[keyof T];
Example
In the example below, we introduce a UserProfile
interface with various properties. Using NonNullableKeys
, we implement a prepareProfileUpdate
function. This function filters out nullable properties from a user profile update object, ensuring that only properties with meaningful (non-null
/undefined
) values are included in the update payload. This approach can be especially valuable in API interactions where avoiding null
/undefined
data submission is desired for maintaining data integrity in backend systems.
interface UserProfile {
id: string;
name: string | null;
email?: string | null;
bio?: string;
lastLogin: Date | null;
}
function prepareProfileUpdate<T extends object>(
profile: T
): Pick<T, NonNullableKeys<T>> {
const updatePayload: Partial<T> = {};
(Object.keys(profile) as Array<keyof T>).forEach(key => {
const isValuePresent = profile[key] !== null &&
profile[key] !== undefined;
if (isValuePresent) {
updatePayload[key] = profile[key];
}
});
return updatePayload as Pick<T, NonNullableKeys<T>>;
}
const userProfileUpdate: UserProfile = {
id: "123",
name: "John Doe",
email: null,
bio: "Software Developer",
lastLogin: null,
};
const validProfileUpdate = prepareProfileUpdate(
userProfileUpdate
);
// Output:
// { id: "123", name: "John Doe", bio: "Software Developer" }
console.log(validProfileUpdate);
JSONObject
The JSONObject
type is useful for defining the shape of objects that can be converted to or from JSON without loss or when interfacing with APIs that communicate using JSON. It employs a construction which is known as a recursive type or mutual recursion, wherein two or more types depend on each other. Recursive types are useful for describing data structures that can nest within themselves to an arbitrary dept.
type JSONObject = { [key: string]: JSONValue };
type JSONValue = string | number | boolean | null | JSONObject |
JSONValue[];
Example
In this example, we define a configuration object for an application. This configuration object must be serializable to JSON, so we enforce its shape to conform to the JSONObject
type.
function saveConfiguration(config: JSONObject) {
const serializedConfig = JSON.stringify(config);
// In a real application, this string could be saved to a file,
// sent to a server, etc.
console.log(`Configuration saved: ${serializedConfig}`);
}
const appConfig: JSONObject = {
user: {
name: "John Doe",
preferences: {
theme: "dark",
notifications: true,
},
},
version: 1,
debug: false,
};
saveConfiguration(appConfig);
OptionalExceptFor
The OptionalExceptFor
type is a utility type that takes an object type T
and a set of keys TRequiredKeys
from T
, making all properties optional except for the specified keys. It's useful in scenarios where most properties of an object are optional, but a few are mandatory. This type facilitates a more flexible approach to typing objects without having to create multiple interfaces or types for variations of optional properties, especially in configurations, where only a subset of properties is required to be present.
type OptionalExceptFor<T, TRequiredKeys extends keyof T> =
Partial<T> & Pick<T, TRequiredKeys>;
Example
In the following example, the OptionalExceptFor
type is used to define an interface for user settings where only the userId
is required, and all other properties are optional. This allows for more flexible object creation while ensuring that the userId
property must always be provided.
interface UserSettings {
userId: number;
notifications: boolean;
theme: string;
language: string;
}
type SettingsWithMandatoryID =
OptionalExceptFor<UserSettings, 'userId'>;
const userSettings: SettingsWithMandatoryID = {
userId: 123,
// Optional: 'notifications', 'theme', 'language'
theme: 'dark',
};
function configureSettings(
settings: SettingsWithMandatoryID
) {
// Configure user settings logic
}
configureSettings(userSettings);
ReadonlyDeep
The ReadonlyDeep
type is a utility that makes all properties of a given type T
read-only, deeply. This means that not only are the top-level properties of the object made immutable, but all nested properties are also recursively marked as read-only. This type is particularly useful in scenarios where immutability is paramount, such as in Redux state management, where preventing unintended state mutations is crucial.
type ReadonlyDeep<T> = {
readonly [P in keyof T]: T[P] extends object ?
ReadonlyDeep<T[P]> : T[P];
};
Example
The following example ensures that neither the Person
object itself nor any of its nested properties can be modified, thus demonstrating how the ReadonlyDeep
type can be applied to ensure deep immutability.
interface Address {
street: string;
city: string;
}
interface Person {
name: string;
age: number;
address: Address;
}
const person: ReadonlyDeep<Person> = {
name: "Anton Zamay",
age: 25,
address: {
street: "Secret Street 123",
city: "Berlin",
},
};
// Error: Cannot assign to 'name' because it is a read-only
// property.
person.name = "Antonio Zamay";
// Error: Cannot assign to 'city' because it is a read-only
// property.
person.address.city = "San Francisco";
PartialDeep
The PartialDeep
type recursively makes all properties of an object type T
optional, deeply. This type is particularly useful in scenarios where you're working with complex nested objects and need a way to partially update or specify them. For example, when handling state updates in large data structures without the need to specify every nested field, or when defining configurations that can override defaults at multiple levels.
type PartialDeep<T> = {
[P in keyof T]?: T[P] extends object ? PartialDeep<T[P]> :
T[P];
};
Example
In the example below, we use the PartialDeep
type to define a function updateUserProfile
that can accept partial updates to a user profile, including updates to nested objects such as address
and preferences
.
interface UserProfile {
username: string;
age: number;
address: {
street: string;
city: string;
};
preferences: {
newsletter: boolean;
};
}
function updateUserProfile(
user: UserProfile,
updates: PartialDeep<UserProfile>
): UserProfile {
// Implementation for merging updates into user's profile
return { ...user, ...updates };
}
const currentUser: UserProfile = {
username: 'johndoe',
age: 30,
address: {
street: '123 Elm St',
city: 'Anytown',
},
preferences: {
newsletter: true,
},
};
const userProfileUpdates: PartialDeep<UserProfile> = {
address: {
city: 'New City',
},
};
const updatedProfile = updateUserProfile(
currentUser,
userProfileUpdates
);
Brand
The Brand
type is a TypeScript utility that employs nominal typing for otherwise structurally identical types. TypeScript’s type system is structural, meaning that two objects are considered the same type if they have the same shape, regardless of the names or locations of their declarations. However, there are scenarios where treating two identically shaped objects as distinct types is beneficial, such as differentiating between types that are essentially the same but serve different purposes (e.g., user IDs and order IDs both being strings but representing different concepts). The Brand
type works by intersecting a type T
with a unique branding object, effectively differentiating otherwise identical types without changing the runtime behavior.
type Brand<T, B> = T & { __brand: B };
Example
Imagine an application where both user IDs and order IDs are represented as strings. Without branding, these could be confused, leading to potential bugs. Using the Brand
type, we can create two distinct types, UserId
and OrderId
, making it impossible to mistakenly assign one to the other.
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
function fetchUserById(id: UserId) {
// Fetch user logic here
}
function fetchOrderByOrderId(id: OrderId) {
// Fetch order logic here
}
// Creation of branded types
const userId: UserId = '12345' as UserId;
const orderId: OrderId = '67890' as OrderId;
// These calls are safe and clear
fetchUserById(userId);
fetchOrderByOrderId(orderId);
// Error: Argument of type 'OrderId' is not assignable
// to parameter of type 'UserId'
fetchUserById(orderId);
Top comments (12)
Quite handy stuff for Typescript! The
JSONObject
caught my attention particularly, might come handy in form submitting scenarios, for example. Nice read 👍Instead of NonNullableKeys, you should create a type that lets you pass in any type instead of Nullish, and then if you want non nullable keys, you just pass in Nullish. That way you can also reuse it for other types, not just Nullish.
Good suggestion! I will provide corresponding types if you mentioned the more general solution anyways. These types are basically extensions of
Pick
andOmit
, but with theCondition
:I love this article so much.
Really, nice article, but the ReadOnlyDeep utility needs some more work. It does not cover arrays.Example:Hey, thanks for your comment!
If a property's type is an object (which includes arrays, as arrays are objects in JavaScript), the type recursively applies the
ReadonlyDeep
transformation to that object. So it should work.Try
ReadonlyDeep
instead ofReadonly
in your example.Ooops, you are completely right of course! I was comparing out a few different things in the playground and didn’t notice I made the switch. I was really confused too since I know I saw you used a recursive type – for a moment I thought this was a TS bug!
That's ok, always happens with me too :D
Great article! Thanks!
The NonNullableKeys example doesn't work for me (TS playground, 5.6.3 with strictNullChecks enabled). Optional keys and keys with nullish values were still valid within the type. I had more success by comparing T[K] and whether it fits inside its NonNullable variant.
Works very nicely in
OptionalExceptFor<T, NonNullableKeys<T>>
.The only issue I discovered is related to optional keys (test case 4 doesn't pass). However, it's easy to fix by removing the optional modifiers from the properties using the
-?
operator in the mapped type. This ensures thatT[K]
reflects the declared type without the implicitundefined
:typescriptlang.org/play/?#code/C4T...
Ah, I misunderstood the use case:
I was looking for 'exclude all keys that contain Nullish', while your implementation works by 'include all keys that contain not Nullish'.
E.g.
key1: string | undefined
should be rejected in my implementation, while it should be accepted in yours.Thanks for sharing the playground and the article, it still helped me to get to the solution I needed!
P.S. If exactOptionalPropertyTypes is set to true in tsconfig, this also prevents the implicit undefined, without stripping the optionality away. This in turn is really great make a 'minimum template' for an object in combination with the OptionalExceptFor helper: always define the keys that may not be nullish, and if you define more, they may not have the value null or undefined either.