TypeScript has gained immense popularity as a statically-typed superset of JavaScript, allowing developers to catch errors early, improve code quality, and enhance maintainability. One of TypeScript’s most powerful features is its utility types, which provide a versatile toolkit for working with types in a concise and expressive manner. In this article, we will delve into some essential TypeScript utility types that every developer should be familiar with, along with relevant code examples.
1. Partial<T>
and Required<T>
– Controlling Property Optionality
The Partial<T>
utility type allows you to create a new type with all properties of T
set as optional. This is incredibly useful when dealing with forms or API payloads where some fields may not be required. Conversely, the Required<T>
utility type does the opposite, ensuring that all properties of T
are mandatory.
interface User {
id: number;
name: string;
email: string;
}
const partialUser: Partial<User> = { name: "John" }; // Valid
const requiredUser: Required<User> = { id: 1, name: "Jane", email: "jane@example.com" }; // Valid
2. Pick<T, K>
and Omit<T, K>
– Selecting and Excluding Properties
The Pick<T, K>
utility type allows you to create a type containing only the specified properties from T
. On the other hand, the Omit<T, K>
utility type lets you create a new type that omits the specified properties.
interface Product {
id: number;
name: string;
price: number;
description: string;
}
type ProductInfo = Pick<Product, "name" | "price">;
type ProductPreview = Omit<Product, "description">;
const productInfo: ProductInfo = { name: "Widget", price: 19.99 }; // Valid
const productPreview: ProductPreview = { id: 1, name: "Gadget", price: 29.99 }; // Valid
3. Record<K, T>
– Creating a Dictionary
The Record<K, T>
utility type constructs a type with keys of type K
and values of type T
. It’s particularly handy for defining dictionaries or lookup tables.
type Fruit = "apple" | "banana" | "orange";
type FruitInventory = Record<Fruit, number>;
const inventory: FruitInventory = {
apple: 10,
banana: 5,
orange: 8,
};
4. ReturnType<T>
and Parameters<T>
– Extracting Function Signatures
The ReturnType<T>
utility type extracts the return type of a function type T
, while Parameters<T>
extracts the parameter types as a tuple from function type T
.
type MathOperation = (a: number, b: number) => number;
type OperationResult = ReturnType<MathOperation>; // number
type OperationParameters = Parameters<MathOperation>; // [number, number]
const add: MathOperation = (a, b) => a + b;
5. Exclude<T, U>
and Extract<T, U>
– Filtering Union Types
The Exclude<T, U>
utility type produces a type that includes all members of T
except those that are assignable to U
. Conversely, Extract<T, U>
constructs a type that includes only the members of T
that are assignable to U
.
type AllColors = "red" | "blue" | "green";
type PrimaryColors = "red" | "blue";
type NonPrimaryColors = Exclude<AllColors, PrimaryColors>; // "green"
type OnlyPrimaryColors = Extract<AllColors, PrimaryColors>; // "red" | "blue"
6. NonNullable<T>
– Removing null
and undefined
The NonNullable<T>
utility type transforms a type T
by removing null
and undefined
from its domain.
type NullableString = string | null | undefined;
type NonNullableString = NonNullable<NullableString>; // string
7. Required<T>
and Partial<T>
– Deep Modifiers
Both Required
and Partial
utility types can be used in a nested manner to modify properties within nested objects or arrays.
interface NestedData {
info: {
name?: string;
age?: number;
};
items: Array<{ id?: number; name?: string }>;
}
type RequiredNestedData = Required<NestedData>;
type PartialNestedData = Partial<NestedData>;
8. ReturnType
and InstanceType
– Extracting Return and Instance Types
The ReturnType<T>
utility type has already been mentioned, but it’s worth highlighting that it’s not just limited to functions. You can also use it to extract the return type of a constructor function, which is useful for working with classes.
class Person {
constructor(public name: string, public age: number) {}
}
type PersonConstructor = typeof Person;
type PersonInstance = InstanceType<PersonConstructor>;
const person: PersonInstance = new Person("Alice", 30); // Valid
9. Keyof
and Valueof
– Extracting Keys and Values
The keyof
keyword is used to extract the keys of an object type as a string literal union. This is useful for creating dynamic type-safe operations on object properties.
interface Car {
brand: string;
model: string;
year: number;
}
type CarKeys = keyof Car; // "brand" | "model" | "year"
On the other hand, there is no built-in valueof
keyword in TypeScript, but you can achieve a similar effect using indexed access types.
type CarValues = Car[keyof Car]; // string | number
10. Parameters
and ConstructorParameters
– Handling Function and Constructor Arguments
While the Parameters<T>
utility type extracts the parameter types of a function, the ConstructorParameters<T>
utility type extracts the parameter types of a constructor function.
type MathOperation = (a: number, b: number) => number;
type OperationParameters = Parameters<MathOperation>; // [number, number]
class Greeter {
constructor(public message: string) {}
}
type GreeterConstructorParams = ConstructorParameters<typeof Greeter>; // [string]
11. ReturnType
and Generics – Higher-Order Functions
You can combine the ReturnType
utility type with generics to create higher-order functions that infer and maintain the correct return type based on the input function.
function withLogging<T extends (...args: any[]) => any>(fn: T): (...args: Parameters<T>) => {
result: ReturnType<T>;
timestamp: Date;
} {
return (...args: Parameters<T>) => {
const result = fn(...args);
return { result, timestamp: new Date() };
};
}
const loggedAdd = withLogging(add);
const result = loggedAdd(3, 5);
// result: { result: 8, timestamp: Date }
12. Distributive Conditional Types
Distributive conditional types allow you to transform union types and infer types more accurately. For instance, T extends U ? X : Y
applied to a union type A | B
will distribute the condition over each member, creating A extends U ? X : Y | B extends U ? X : Y
.
type ExtractProps<T, U> = T extends U ? T : never;
type StringOrNumber = string | number;
type StringProps = ExtractProps<StringOrNumber, string>; // string
type NullableStringOrNumber = string | number | null;
type NonNullableProps = ExtractProps<NullableStringOrNumber, null>; // number | string
Conclusion
In this continuation of our exploration of TypeScript utility types, we’ve covered more essential tools that can greatly enhance your development process. These utility types provide advanced ways to manipulate types, enabling you to create more precise, type-safe, and expressive code. By mastering these utility types, you’ll be equipped to handle a wide range of type-related challenges and scenarios in your TypeScript projects. As you continue to deepen your understanding of these tools, you’ll find yourself writing more robust and maintainable code with TypeScript. Happy coding and type-checking!