TypeScript Advanced Type Inferences
Andrew Pettigrew
Software Engineer, Backend | Building scalable, high-performance systems with AWS, Java, JavaScript, ReactJS and PHP
TypeScript offers a rich set of tools for crafting custom types. We can define new types, build upon existing ones through inheritance, and even use generics to create types that adapt to a variety of data. These features, when combined, empower us to create sophisticated type definitions. We can define types that depend on others, or that are based on a subset of another type's properties. We can even reshape a type by adding or removing properties as needed.
In this article, we'll delve into more advanced type inference techniques.
Type aliases give us a way to name and reuse complex type definitions. But they become truly powerful when we bring generics into the mix, letting us build types dynamically based on other types. If we add the keyof keyword to the mix, we gain the ability to create new types that are specifically based on the properties of another type.
interface IRequired {
a: number;
b: string;
}
// Create a variable ab that conforms to the IAbRequired interface
let ab: IRequired = {
a: 1,
b: "test",
};
// Define a generic type WeakInterface that makes all properties of a given type optional
type WeakInterface<T> = {
[K in keyof T]?: T[K];
};
// Create a variable allOptional of type WeakInterface<IRequired> and initialize it as an empty object
let allOptional: WeakInterface<IRequired> = {};
Note that even though we are making each property in the type?IRequired?optional, we can’t define properties that are not available on this original type.
Partial Mapped Types
The?WeakType?type alias that we created earlier is actually called?"Partial". Constructs a type with all properties of?Type?set to optional.
type Partial<T> = {
[P in keyof T]?: T[P];
};
// Example
interface User {
id: number;
name: string;
email: string;
}
const updateUser = (user: User, updates: Partial<User>): User => {
return { ...user, ...updates };
};
const user: User = {
id: 1,
name: "John Doe",
email: "[email protected]",
};
const updatedUser = updateUser(user, { name: "Jane Doe" });
console.log(updatedUser);
// Output: { id: 1, name: "Jane Doe", email: "[email protected]" }
Required Mapped Types
There is also a mapped type named?"Required" which will do the opposite of?Partial?and mark each property as required:
type Required<T> = {
[P in keyof T]-?: T[P];
};
// Example
interface User {
id: number;
name?: string;
email?: string;
}
const getUserInfo = (user: Required<User>): void => {
console.log(`ID: ${user.id}, Name: ${user.name}, Email: ${user.email}`);
};
const user: Required<User> = {
id: 1,
name: "John Doe",
email: "[email protected]",
};
getUserInfo(user);
// ? ERROR: Missing required properties
const invalidUser: Required<User> = { id: 2 };
// Type '{ id: number; }' is missing the following properties from type 'Required<User>': name, email
Readonly Mapped Types
Similarly, we can use the?Readonly?mapped type to mark each property as?readonly?as follows:
领英推荐
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
Picked mapped Types
The?Pick?mapped type is used to construct a type based on a subset of properties of another type.
interface IAbc {
a: number;
b: string;
c: boolean;
}
// Define a new type PickAb using the Pick utility type to select only the "a" and "b" properties from the IAbc interface.
type PickAb = Pick<IAbc, "a" | "b">;
let pickAbObject: PickAb = {
a: 1,
b: "test",
};
Record Mapped Types
The?Record?mapped type, which is used to construct a type on the fly. This type is essentially the inverse of the Pick mapped type. Instead of choosing specific properties, it requires a defined set of properties, specified as string literals, to be present in the type.
type RecordedCd = Record<"c" | "d", number>;
// Declare a variable of type RecordedCd and assign it an object with properties "c" and "d"
let recordedCdVar: RecordedCd = {
c: 1,
d: 1,
};
Omit Mapped Types
Constructs a type by picking all properties from?Type?and then removing?Keys
interface User {
id: number;
name: string;
email: string;
password: string;
}
// Create a type that excludes "password"
type PublicUser = Omit<User, "password">;
const user: PublicUser = {
id: 1,
name: "John Doe",
email: "[email protected]",
// password: "secret123" // ? ERROR: Property 'password' does not exist on type 'PublicUser'.
};
console.log(user);
TypeScript's advanced type inference capabilities, particularly the combination of type aliases, generics, and the keyof keyword, provide developers with a powerful toolkit for crafting highly expressive and maintainable type systems. By leveraging these features, we can move beyond simple type definitions and create complex, dynamic types that accurately reflect the structure and behavior of our data. For a full comprehensive list of the different types you can make checkout TypeScript Utility section. https://www.typescriptlang.org/docs/handbook/utility-types.html