The Apify SDK supports TypeScript by covering public APIs with type declarations. This allows writing code with auto-completion for TypeScript and JavaScript code alike. This article was written to provide you with a deeper knowledge of TypeScript. But if you want to know more about how it compares with JavaScript for web scraping, you might like to read TypeScript vs. JavaScript: which to use for web scraping?
What is TypeScript?
TypeScript is a superset of JavaScript that provides you with the capabilities of static type-checking, enabling you to catch type-related errors during development. One very useful feature of TypeScript is being able to define and manipulate types effectively to write code that is maintainable and reliable.
Utility types in TypeScript play a significant role in this regard, as they enable you to create new types based on existing ones.
What are utility types?
Utility types are sets of built-in generic types in TypeScript that allow you to create, manipulate, or create new types by applying specific modifications and transformations to existing types.
Implementing utility types in your TypeScript project can make working with types more flexible, expressive, and reusable. Declaring your own custom utility types or using TypeScript's built-in utility types are the two approaches to adding utility types to your code base.
TypeScript built-in utility types
There are different built-in utility types that come along with the TypeScript language to make type transformations without you needing to install a library or create a custom type to use them. Lets look at some of them:
1. Partial<Type>
When defined, the partial utility type in TypeScript turns all of a type's properties into optional fields. This allows you to modify the type's fields in part without TypeScript throwing errors.
For example, say you have User
data in your application and want to update the information of the user. You only want to update specific fields and not the whole data. Using the Partial
utility type, you can transform the data fields of the User
object from required fields to optional fields.
// Description of the user datainterface User { id: number; firstName: string; lastName: string; email: string; bio: string;};// This is the user data fetchedconst userData: User = { id: 12345, firstName: "Jamin", lastName: "Doe", email: "hellojamin@test.com", bio: "Legendary Gamer and everything in between",};//Function to update the user infoconst updateUserInfo = (userId: number, updatedInfo: Partial<User>) => { // Logic to update the user's info with the provided data // Using the partial type, all fields becomes optional updatedInfo.lastName; //(property) lastName?: string | undefined updatedInfo.email; //(property) email?: string | undefined};// Example usageconst userId = 12345;const updatedInfo: Partial<User> = { firstName: "John", bio: "Web Developer",};updateUserInfo(userId, updatedInfo);
In the example above, the Partial
utility type converts all the User
object properties to optional fields.
2. Required<Type>
The Required
utility type is the opposite of the Partial
utility type. It transforms all the fields of your Type
into required fields. Imagine you have a data type of UserRegistration
with some optional fields, but youd like to make all the fields required when using the data. You can achieve this using the Required
utility type.
interface UserRegistration { username?: string; password?: string; email: string; fullName?: string;}const registerUser = (userData: Required<UserRegistration>) => { // Logic to register the user with the provided data};// Example usageconst userData: Required<UserRegistration> = { email: "user@example.com",};// throws an error// Type '{ email: string; }' is missing the following properties from type 'Required<UserRegistration>': username, password, fullNameregisterUser(userData);
3. Pick<Type, Keys>
This utility type enables you to selectively pick properties from a Type
using the Keys
properties of the object you want to pick from.
Lets say you have a product catalog in your application and would like to list available products for a catalog without showing all the data properties of the products. You can create a simplified version of the original product object for listing available products.
// Let's create a simplified version of the product for the product listingtype SimplifiedProduct = Pick<Product, "id" | "name" | "price" | "category">;const product: Product = { id: "1001", name: "Laptop", description: "High-performance laptop", price: 1200, category: "Electronics", stock: 10 // ...};const simplifiedProduct: SimplifiedProduct = { id: product.id, name: product.name, price: product.price, category: product.category};console.log(simplifiedProduct);// Output: { id: "1001, name: "Laptop", price: 1200, category: "Electronics" }
The Pick
utility type can be very useful when you have a very complex data object and only want to display a fraction of that data object to your users.
4. Omit<Type, Keys>
The Omit<Type, Keys>
type removes specified Keys
properties from the Type
and provides you with a Type
without those properties.
// Let's create a simplified version of the product by excluding certain propertiestype SimplifiedProduct = Omit<Product, "description" | "stock">;const product: Product = { id: "1001", name: "Laptop", description: "High-performance laptop", price: 1200, category: "Electronics", stock: 10 // ...};//description and stock are excluded from the product propertiesconst simplifiedProduct: SimplifiedProduct = { id: product.id, name: product.name, price: product.price, category: product.category};console.log(simplifiedProduct);// Output: { id: "1001, name: "Laptop", price: 1200, category: "Electronics" }
5. ReturnType<Type>
The return type is used to get a functions return type. This enables you to extract and use the type that a function will return when invoked. It works by taking in a function as its parameter and returning the Type
of the returned value of the function.
Consider the fetchDataFromApi
example below and how the return type of the function was derived using the ReturnType
utility type.
type ApiResponse = { success: boolean; data: any; // Assuming data can be of any type};type ApiFetchFunction = () => Promise<ApiResponse>;function fetchDataFromApi(endpoint: string): ApiFetchFunction { // Simulating fetching data from the API const fetchFunction: ApiFetchFunction = async () => { const response = await fetch(endpoint); const data = await response.json(); return { success: true, data }; }; return fetchFunction;}// Usageconst fetchProductData = fetchDataFromApi('https://api.example.com/products');// Get the return type from the functiontype ProductDataResponse = ReturnType<typeof fetchProductData>;// Use the function's return typeasync function handleProductData() { const response: ProductDataResponse = await fetchProductData(); console.log('Product data:', response.data);}
6. Awaited<Type>
The Awaited
type is used for asynchronous functions and operations to determine the data type that the function or operation would resolve. Consider the product example you used previously. Imagine you need to fetch the product data from an API. The API service returns a Promise
with the product data. You want to handle this asynchronously and extract the type of the resolved data.
// A function that simulates fetching product data from an APIconst fetchProductFromAPI = (): Promise<Product> => { return new Promise(resolve => { // Simulate an asynchronous API call setTimeout(() => { resolve({ id: "1001", name: "Laptop", description: "High-performance laptop", price: 1200, category: "Electronics", stock: 10 }); }, 1000); });};// Use ReturnType to get the return type of the async function and use// Awaited to retrieve the type of the async callconst getProductData = async (): Promise<Awaited<ReturnType<typeof fetchProductFromAPI>>> => { const productData = await fetchProductFromAPI(); return productData;};getProductData().then(data => { console.log("Product data:", data); /* Output: Product data: { id: '1001', name: 'Laptop', description: 'High-performance laptop', price: 1200, category: 'Electronics', stock: 10 } */});
It's interesting to note that the Awaited
utility transforms types in a recursive manner. So, no matter how deeply nested a Promise
is, it will always resolve its value. For example, the code below would transform the nested async request to a single data type.
type Data = Awaited<Promise<Promise<string>>>;//type Data = string
7. Record<Keys, Type>
The Record
utility type enables you to construct an object type with Keys
as its property keys and Type
as its property values. The Keys
passed to the Record
ensure that only those specific keys can be assigned values of Type
. This is particularly useful when you want to narrow down your records by only accepting specific keys.
Lets use a real-world scenario to understand this better:
You're creating a notification system for an application that notifies your users based on specific actions they take or responses to a request they make.
You can use the Record
utility like this:
type NotificationTypes = 'error' | 'success' | 'warning';type IconTypes = 'errorIcon' | 'successIcon' | 'warningIcon';type IconColors = 'red' | 'green' | 'yellow';const notificationIcons: Record< NotificationTypes, { iconType: IconTypes; iconColor: IconColors }> = { error: { iconType: 'errorIcon', iconColor: 'red' }, success: { iconType: 'successIcon', iconColor: 'green' }, warning: { iconType: 'warningIcon', iconColor: 'yellow' }};console.log(notificationIcons.error)// OUTPUT: { iconType: 'errorIcon', iconColor: 'red' }
In the example above, you specified the Keys
for the object to be a Union
type of either error
, success
, or warning
and assigned them to a property value of { iconType: IconTypes; iconColor: IconColors }
.
With this, youve created a constraint of records where the notificationIcons
can only be accessed by one of the NotificationTypes
. Trying to access the notificationIcons
without a known NotificationTypes
will throw an error:
console.log(notificationIcons.completed)// Property 'completed' does not exist on type 'Record<NotificationTypes, { iconType: IconTypes; iconColor: IconColors; }>'
8. Readonly<Type>
The Readonly
type transforms the object properties of a Type
to 'read-only' so its values cannot be reassigned after initialization.
Example: Suppose you have a configuration object for a web application with various settings, and you want to ensure that once the configuration is set, it cannot be modified.
interface AppConfig { apiUrl: string; maxRequestsPerMinute: number; analyticsEnabled: boolean;}const initialConfig: Readonly<AppConfig> = { apiUrl: '<https://api.example.com>', maxRequestsPerMinute: 1000, analyticsEnabled: true,};// Attempt to modify a property (will result in a TypeScript error)initialConfig.apiUrl = '<https://new-api.example.com>';// Error: Cannot assign to 'apiUrl' because it is a read-only property.function displayConfig(config: Readonly<AppConfig>) { console.log('API URL:', config.apiUrl); console.log('Max Requests Per Minute:', config.maxRequestsPerMinute); console.log('Analytics Enabled:', config.analyticsEnabled);}displayConfig(initialConfig);
9. NonNullable<Type>
The NonNullable
type transforms a type by removing all null
and undefined
from the input Type
passed to it.
Example: Let's say you have a union type that accepts a string
, or a number
, as values. The type can sometimes be (undefined
or null
) as optional values. In the case where you dont want to accept null
or undefined
when reusing this type, you can stripe off the null
and undefined
fields using NonNullable
.
type UserID = string | number | null | undefined//Works fine as expectedconst userByIDString = "100"// error: Type 'undefined' is not assignable to type 'NonNullable<UserID>'const userByID: NonNullable<UserID> = undefined//Works fine as expectedconst userByIDNumber: NonNullable<UserID> = 102
For a list of all the available built-in utility types, check out the official TypeScript documentation.
Custom types in TypeScript
Aside from using the built-in utility type from TypeScript, the language also offers the flexibility to create custom utility types to suit your needs where needed. Lets create a custom type in TypeScript that you can use to transform other types. For this example, youll create a utility type that accepts Type
as an object type and filters the keys of the object by the Key
passed to the utility type.
// Custom utility type declaration type FilterKeysByType<Type, KeyType> = { [key in keyof Type as Type[key] extends KeyType ? key: never]: Type[key];}// Usage exampleinterface Person { name: string; age: number; email: string; isAdmin: boolean;}type StringKeys = FilterKeysByType<Person, string>;//OUTPUT: { name: string, email: string }
Lets break down the custom utility line by line to understand it better,
type FilterKeysByType<Type, KeyType>
is the type definition, and it accepts two things: theType
you want to transform and theKeyType
to filter by.In the second line, three major things are happening:
Custom types vs. utility types
Should you create custom types over TypeScript's built-in utility types?
That depends on the level of transformation and abstraction you're performing. If you want to perform complex transformations that go beyond what TypeScript's built-in features offer, you should consider creating your own custom types.
Utility types are built-in features of TypeScript, and they don't require any external libraries or more lines of code for you to define and use them. Since they're built-in features of TypeScript, they're well-known and familiar to most developers.
Combining custom types with utility types
Utilizing TypeScripts capabilities to create custom types lets you combine a custom type with a utility type to create a customized utility type that is tailored to fit the needs of your project.
Lets explore this with an example. Say you want to create a custom type that transforms another type to make specific properties of that type optional. You can create such a custom type by utilizing TypeScripts already existing utility types to achieve this:
type PartialBy<Type, Key extends keyof Type> = Omit<Type, Key> & Partial<Pick<Type, Key>>;// Usageinterface User {id: string;name: string;email: string;}const partialUser: PartialBy<User, 'email'> = {id: '123',name: 'John Doe',};// email is optional in partialUserconsole.log(partialUser);
In this example, PartialBy
is a utility type that takes two parameters: Type
, which is the original type, and Key
, which represents the keys that you want to make partial. It uses Omit
to remove the specified keys from the original type and Partial<Pick>
to make those keys optional.
With this, the PartialBy
custom utility can transform the Key
of any Type
passed to it.
Summing up: why you should use utility types
Utility types in TypeScript can help you write sturdy, maintainable types. You can use them to make your type declarations more flexible, expressive, and reusable. They also give you the ability to combine them with custom types to create more powerful type declarations.
Top comments (0)