Home Blog Page 36

Utility Types Part 1: Partial, Required, Readonly

0
typscript course
typscript course

Table of Contents

  • Introduction
  • What Are Utility Types in TypeScript?
  • Overview of Partial, Required, and Readonly Utility Types
  • Partial<T>
    • Definition and Syntax
    • Use Cases and Examples
  • Required<T>
    • Definition and Syntax
    • Use Cases and Examples
  • Readonly<T>
    • Definition and Syntax
    • Use Cases and Examples
  • Practical Examples
    • Example 1: Making Properties Optional with Partial
    • Example 2: Ensuring All Properties Are Required with Required
    • Example 3: Protecting Object Properties with Readonly
  • When and Why to Use Utility Types
  • Conclusion

Introduction

TypeScript provides a set of built-in utility types that allow developers to modify types in various ways. These utility types make it easier to work with types in a flexible yet type-safe manner, without manually creating type transformations. In this article, we will dive into the first part of TypeScript’s utility types: Partial, Required, and Readonly. Each of these utility types provides a powerful tool for adjusting properties in types, which can be especially helpful for working with objects and ensuring type safety across your application.


What Are Utility Types in TypeScript?

Utility types are predefined types that can be applied to other types to modify their properties or behavior. They are part of TypeScript’s standard library and enable developers to quickly adapt types to different needs, reducing boilerplate code and increasing productivity.

In this article, weโ€™ll focus on three important utility types:

  • Partial<T>: Makes all properties of a type optional.
  • Required<T>: Makes all properties of a type required.
  • Readonly<T>: Makes all properties of a type read-only.

These utility types work with any type, and they can be particularly useful for transforming and manipulating complex objects.


Overview of Partial, Required, and Readonly Utility Types

Before we dive deeper into each type, let’s have an overview of how these utility types behave:

  1. Partial<T>: Converts all properties in the given type T to be optional.
  2. Required<T>: Converts all properties in the given type T to be required, even if they were previously optional.
  3. Readonly<T>: Converts all properties in the given type T to be read-only, meaning they cannot be reassigned after initialization.

Now, letโ€™s explore each utility type in detail.


Partial<T>

Definition and Syntax

The Partial<T> utility type constructs a type that has all properties of type T set to optional. This is useful when you want to represent a partial version of an object type, such as when updating an object incrementally or performing operations where not all properties are required.

The syntax for Partial is as follows:

type Partial<T> = {
[P in keyof T]?: T[P];
};

In this definition:

  • T is the original type.
  • keyof T is a union of all property names in the type T.
  • T[P] refers to the type of each property in T.
  • The ? makes each property optional.

Use Cases and Examples

  • Updating objects incrementally: When working with objects that may not have all properties defined during an update.
  • Handling API responses: Sometimes, you may receive partial data from an API, where not all properties are available.

Example:

interface User {
id: number;
name: string;
email: string;
}

const updateUser = (user: User, updatedFields: Partial<User>) => {
return { ...user, ...updatedFields };
};

const existingUser: User = { id: 1, name: 'Alice', email: '[email protected]' };

const updatedUser = updateUser(existingUser, { name: 'Alice Updated' });

console.log(updatedUser); // { id: 1, name: 'Alice Updated', email: '[email protected]' }

In this example, the updatedFields parameter is of type Partial<User>, which means only the properties provided are updated, and all other properties remain unchanged.


Required<T>

Definition and Syntax

The Required<T> utility type makes all properties in the given type T required, even if they were previously optional. This is particularly useful when you want to ensure that all properties in an object are defined, such as when performing validation or creating an object that needs to have all fields filled out.

The syntax for Required is:

type Required<T> = {
[P in keyof T]-?: T[P];
};

In this definition:

  • T is the original type.
  • keyof T is the union of property names.
  • T[P] is the type of each property.
  • The -? removes the optional modifier, making each property required.

Use Cases and Examples

  • Ensuring all fields are provided: In situations where you want to ensure an object is fully defined before being used.
  • Validating complete data: For validation, you may want to force all properties of an object to be required.

Example:

interface Product {
name?: string;
price?: number;
}

const createProduct = (product: Required<Product>) => {
console.log(`Product name: ${product.name}, Product price: ${product.price}`);
};

createProduct({ name: 'Laptop', price: 1200 }); // Works fine
createProduct({}); // Error: Property 'name' is missing in type '{}' but required in type 'Required<Product>'.

In this example, the Required<Product> type ensures that both name and price are required properties, and passing an object without them will result in a compilation error.


Readonly<T>

Definition and Syntax

The Readonly<T> utility type makes all properties in the given type T read-only. This means you cannot modify the properties after the object has been created, providing immutability.

The syntax for Readonly is:

type Readonly<T> = {
readonly [P in keyof T]: T[P];
};

In this definition:

  • T is the original type.
  • keyof T is the union of property names.
  • T[P] is the type of each property.
  • The readonly modifier ensures that the property is immutable.

Use Cases and Examples

  • Ensuring immutability: When you want to protect object properties from being modified.
  • Defining constants: For defining configuration objects or constants that should not be modified after creation.

Example:

interface Config {
readonly apiUrl: string;
readonly timeout: number;
}

const config: Readonly<Config> = {
apiUrl: 'https://api.example.com',
timeout: 5000,
};

config.apiUrl = 'https://api.newurl.com'; // Error: Cannot assign to 'apiUrl' because it is a read-only property.

In this example, the Readonly<Config> type ensures that both apiUrl and timeout cannot be modified after the object is created. Any attempt to change their values results in an error.


Practical Examples

Example 1: Making Properties Optional with Partial

You can use Partial when updating a user profile:

interface UserProfile {
name: string;
age: number;
address: string;
}

const updateProfile = (profile: UserProfile, updatedData: Partial<UserProfile>) => {
return { ...profile, ...updatedData };
};

This allows you to update only a subset of the UserProfile properties.

Example 2: Ensuring All Properties Are Required with Required

For a product creation process, you might want to ensure that all fields are provided:

interface Product {
name?: string;
price?: number;
}

const createProduct = (product: Required<Product>) => {
console.log(product);
};

createProduct({ name: 'TV', price: 1000 }); // Works
createProduct({}); // Error: Property 'name' is required.

Example 3: Protecting Object Properties with Readonly

You can use Readonly to prevent modification of critical configuration settings:

interface Config {
readonly appName: string;
readonly version: string;
}

const config: Readonly<Config> = { appName: 'MyApp', version: '1.0' };
config.version = '1.1'; // Error: Cannot assign to 'version' because it is a read-only property.

When and Why to Use Utility Types

  1. Partial: Use Partial when you need to make a subset of object properties optional. This is useful in scenarios where you only need to update part of an object or work with incomplete data.
  2. Required: Use Required when you want to enforce that all properties of an object are present. This is useful in validation or when dealing with objects where you cannot afford any undefined or missing properties.
  3. Readonly: Use Readonly when you need to make an object immutable. This is particularly useful for configuration objects, constants, or data that should not be changed after initialization.

Conclusion

In this first part of our exploration into TypeScriptโ€™s utility types, weโ€™ve learned about Partial, Required, and Readonly. These utility types provide a flexible and powerful way to modify existing types, ensuring better type safety and control over your objects. Whether you need to update objects incrementally, ensure all fields are required, or enforce immutability, these utility types can significantly enhance your development experience.

By leveraging utility types, you can write cleaner and more maintainable code while maintaining the type safety that TypeScript provides.

Template Literal Types: Dynamic String Typing

0
typscript course
typscript course

Table of Contents

  • Introduction
  • What Are Template Literal Types?
  • Basic Syntax of Template Literal Types
  • Combining Static and Dynamic Parts in Template Literals
  • Advanced Usage of Template Literals
    • Creating String Patterns
    • Conditional Types with Template Literals
  • Working with String Literals and Unions
  • Using Template Literal Types with Enums and Other Types
  • Practical Examples
    • Example 1: Enforcing String Patterns
    • Example 2: Combining Union Types and Template Literals
  • Best Practices for Using Template Literal Types
  • Conclusion

Introduction

Template literal types in TypeScript are a powerful feature that allows you to construct string types dynamically by combining literal strings with expressions. They offer the flexibility to define types that match specific patterns of strings, similar to how template literals work in JavaScript, but with the added power of type safety.

This feature can be used to enforce rules on strings, generate types based on patterns, and even combine multiple types into one. By the end of this article, you’ll understand how to leverage template literal types to create more precise and dynamic string typings in TypeScript.


What Are Template Literal Types?

Template literal types enable the creation of string types based on the structure of string literals. They allow you to express types that resemble template literals in JavaScript, but with the added ability to include type expressions.

A template literal type is defined using the `backtick (“) syntax and can combine literal types with placeholders for type expressions, like so:

type Greeting = `Hello, ${string}!`;

In this case, Greeting is a type that can represent any string starting with “Hello, ” and followed by any string, finishing with an exclamation mark.


Basic Syntax of Template Literal Types

The basic syntax for a template literal type is as follows:

type SomeType = `prefix-${string}-suffix`;

Here, SomeType can represent any string that starts with “prefix-“, followed by any string (string), and ends with “-suffix”.

In more detail:

  • ${} allows you to embed expressions or types inside the string template.
  • string is a type that represents any string, but you can use more specific types as needed.

Example:

type FileName = `file-${number}.txt`;

Here, FileName represents any string that matches the pattern of "file-", followed by a number, and ending with ".txt".


Combining Static and Dynamic Parts in Template Literals

Template literal types can combine static string parts (literal text) with dynamic parts (type placeholders). This allows you to enforce specific patterns while still allowing flexibility in the type.

Example:

type DateString = `2022-${'01' | '02' | '03'}-${string}`;

In this example, DateString is a type that expects a string beginning with 2022-, followed by one of the strings “01”, “02”, or “03”, and then a hyphen and any other string. This is useful when you want to represent dates with specific months but without rigidly defining the full date structure.


Advanced Usage of Template Literals

Template literal types can be combined with other TypeScript features, such as conditional types and union types, to create more complex and flexible string patterns.

Creating String Patterns

One common use case of template literal types is to enforce a specific pattern in strings. You can combine literal types, unions, and other types to create a string pattern that matches certain conditions.

type UserRole = `admin-${string}` | `user-${string}`;

In this example, UserRole can be any string that starts with admin- or user- and is followed by any string. This is useful for user roles that require a dynamic identifier but follow a predefined structure.

Conditional Types with Template Literals

Template literal types can also be used with conditional types to create complex patterns based on conditions.

type Status = 'active' | 'inactive';
type StatusMessage = Status extends 'active' ? `User is ${Status}` : `User is ${Status}`;

Here, the StatusMessage type will either be User is active or User is inactive, depending on the value of the Status.


Working with String Literals and Unions

You can combine template literal types with union types to generate even more flexible string patterns. A union of template literal types allows you to define a range of valid string patterns.

Example:

type ButtonType = `primary-${'submit' | 'reset'}` | `secondary-${'submit' | 'reset'}`;

In this case:

  • ButtonType will allow any string that starts with primary- or secondary-, followed by either submit or reset.
  • This can represent different types of buttons in a UI, ensuring that the string matches the pattern for button types.

Using Template Literal Types with Enums and Other Types

Template literal types can also be combined with other types like enums and type aliases to create more complex and reusable patterns.

Example: Template Literals with Enums

enum FileType {
JPEG = 'jpeg',
PNG = 'png',
GIF = 'gif'
}

type FileName = `image-${FileType}.${string}`;

Here:

  • FileName is a type that expects strings starting with image-, followed by one of the values from the FileType enum, and ending with any string (e.g., "image-jpeg.abc").

By combining enums with template literals, you can create dynamic, but well-defined string patterns that represent structured data.


Practical Examples

Example 1: Enforcing String Patterns

type EmailPattern = `${string}@${string}.${'com' | 'org' | 'net'}`;

In this example, EmailPattern enforces a pattern where the string must contain an “@” symbol, followed by a domain name, and then ending with a top-level domain of either “com”, “org”, or “net”.

Example 2: Combining Union Types and Template Literals

type UserId = `${'user' | 'admin'}-${number}`;

This type represents a string that starts with either “user-” or “admin-” and is followed by a number. It’s a common pattern for user identification, ensuring both flexibility and consistency in the string format.

Example 3: Combining Multiple Patterns

type PhoneNumber = `+${number}-${number}-${number}`;

This template literal type enforces a phone number format like +123-456-7890, where each part is a number, but the exact digits are flexible.


Best Practices for Using Template Literal Types

  1. Use Template Literals for Enforcing Patterns: Template literals are most useful when you need to enforce a specific string pattern, such as file names, user IDs, or status codes.
  2. Combine with Union Types for Flexibility: Combine template literals with union types to create more complex patterns and increase the flexibility of your types.
  3. Use with Enums for Strong Typing: When working with predefined sets of values, combining template literal types with enums can provide a type-safe way to generate structured strings.
  4. Be Aware of Performance: While template literals provide great flexibility, using them excessively in very large codebases might impact performance, so use them judiciously in scenarios that require dynamic string typing.

Conclusion

Template literal types are a powerful feature in TypeScript that allow you to define string types dynamically based on patterns. They give you the ability to enforce specific string formats, combine multiple types, and even use conditional logic to determine string content. By combining template literals with union types, enums, and other TypeScript features, you can create flexible and type-safe patterns for a variety of scenarios.

With the knowledge from this article, you can start applying template literal types to your own projects, ensuring better type safety and consistency in your code. Whether you’re enforcing URL formats, file names, or dynamic identifiers, template literal types offer a clean and efficient solution.

Mapped Types: Creating New Types Dynamically

0
typscript course
typscript course

Table of Contents

  • Introduction
  • What Are Mapped Types?
  • Basic Syntax of Mapped Types
  • Common Use Cases of Mapped Types
  • Creating Read-Only and Writable Types
  • Using Mapped Types with keyof and in
  • Mapping with Optional and Required Properties
  • Advanced Mapped Types: Nested Mapped Types
  • Practical Examples
    • Example 1: Creating Read-Only Properties
    • Example 2: Changing Property Types Dynamically
    • Example 3: Mapping Object Keys
  • Best Practices for Using Mapped Types
  • Conclusion

Introduction

Mapped types are one of the most powerful features of TypeScript, allowing you to dynamically create new types by transforming or modifying existing ones. With mapped types, you can iterate over the keys of a given type and transform the properties according to certain rules. They are especially useful for reducing repetitive code and creating reusable utilities for complex types.

In this article, we will dive deep into mapped types, discussing their syntax, common use cases, and advanced techniques. By the end, youโ€™ll understand how to create new types dynamically and apply them to various scenarios.


What Are Mapped Types?

Mapped types allow you to create a new type by iterating over the properties of an existing type. The transformation can be as simple as making all properties optional, read-only, or even changing the type of properties dynamically. Mapped types offer flexibility and help you reduce redundancy in type definitions.

The basic syntax of a mapped type is:

type NewType<T> = {
[K in keyof T]: SomeTransformation<T[K]>;
};

Here:

  • T is the type youโ€™re iterating over.
  • keyof T represents the keys of the type T.
  • SomeTransformation<T[K]> defines how each property should be transformed.

Basic Syntax of Mapped Types

To understand how mapped types work, letโ€™s start with a basic example:

type Person = {
name: string;
age: number;
};

type ReadOnlyPerson = {
readonly [K in keyof Person]: Person[K];
};

Here:

  • Person is a simple object type with two properties: name and age.
  • ReadOnlyPerson is a new type where all properties of Person are transformed to be read-only using the readonly keyword.

This creates a new type where the properties are immutable:

const person: ReadOnlyPerson = {
name: "John",
age: 30
};

person.name = "Jane"; // Error: Cannot assign to 'name' because it is a read-only property.

In this case, the mapped type dynamically applies the readonly modifier to all properties of Person.


Common Use Cases of Mapped Types

Mapped types are incredibly useful in a variety of situations, including:

  • Immutability: Making all properties of a type read-only or writable.
  • Optional Properties: Transforming all properties into optional ones.
  • Mapping Property Types: Changing the types of certain properties dynamically.
  • Creating Flexible Object Structures: Defining structures that adapt based on certain conditions.

By using mapped types, you can create flexible and reusable patterns for various common tasks, reducing code duplication.


Creating Read-Only and Writable Types

One of the most common use cases for mapped types is making all properties of an object either read-only or writable. TypeScript provides built-in utility types like Readonly and Writable, but you can easily recreate these behaviors using mapped types.

Example: Creating a Read-Only Type

type Person = {
name: string;
age: number;
};

type ReadOnlyPerson = {
readonly [K in keyof Person]: Person[K];
};

Here, ReadOnlyPerson will have all properties of Person marked as readonly. You can use this technique to apply immutability to any type.

Example: Creating a Writable Type

type ReadOnlyPerson = {
readonly name: string;
readonly age: number;
};

type WritablePerson = {
-readonly [K in keyof ReadOnlyPerson]: ReadOnlyPerson[K];
};

In this example, the WritablePerson type removes the readonly modifier from the properties of ReadOnlyPerson, making them writable again.


Using Mapped Types with keyof and in

In a mapped type, keyof is used to get the keys of a type, and in is used to iterate over those keys. This combination makes it easy to map over every property of an object and apply a transformation.

Example: Using keyof and in for Mapping

type Person = {
name: string;
age: number;
};

type MappedPerson = {
[K in keyof Person]: string;
};

Here:

  • We use keyof Person to get all the keys of the Person type (name and age).
  • We iterate over these keys using in, and for each key, we set its type to string.

As a result, MappedPerson will have all properties of Person with the type string:

type MappedPerson = {
name: string;
age: string;
};

This technique allows you to transform all properties of a type in a concise and reusable way.


Mapping with Optional and Required Properties

Mapped types also allow you to create types where all properties are either optional or required.

Example: Making All Properties Optional

type Person = {
name: string;
age: number;
};

type OptionalPerson = {
[K in keyof Person]?: Person[K];
};

Here, the OptionalPerson type has all properties of Person marked as optional.

Example: Making All Properties Required

type OptionalPerson = {
name?: string;
age?: number;
};

type RequiredPerson = {
[K in keyof OptionalPerson]-?: OptionalPerson[K];
};

In this example:

  • RequiredPerson is a mapped type that makes all properties of OptionalPerson required again.

Advanced Mapped Types: Nested Mapped Types

You can use mapped types to create deeply nested types as well. This allows you to transform complex nested structures dynamically.

Example: Nested Mapped Types

type Person = {
name: string;
address: {
street: string;
city: string;
};
};

type ReadOnlyPerson = {
readonly [K in keyof Person]: Person[K] extends object ? ReadOnlyPerson[Extract<keyof Person[K], string>] : Person[K];
};

In this example:

  • The mapped type recursively marks all properties of the nested address object as readonly.

This type transforms every property in the object and its nested objects, providing deep immutability.


Practical Examples

Example 1: Creating Read-Only Properties

type Person = {
name: string;
age: number;
};

type ReadOnlyPerson = {
readonly [K in keyof Person]: Person[K];
};

In this example:

  • ReadOnlyPerson will have all properties of Person as readonly.

Example 2: Changing Property Types Dynamically

type Person = {
name: string;
age: number;
};

type MappedPerson = {
[K in keyof Person]: string;
};

Here:

  • MappedPerson dynamically changes all properties of Person to string type.

Example 3: Mapping Object Keys

type Person = {
name: string;
age: number;
};

type MappedPerson = {
[K in keyof Person]: Person[K] extends string ? number : Person[K];
};

In this case:

  • We change the type of name to number if the property type is string.

Best Practices for Using Mapped Types

  1. Keep It Simple: While mapped types are powerful, avoid overly complex transformations that make the code hard to understand and maintain.
  2. Use Built-in Utility Types When Possible: TypeScript has many built-in utility types like Readonly, Partial, and Required. Use these when they fit your needs, as they are well-tested and optimized.
  3. Be Careful with Recursive Mapped Types: When dealing with deeply nested structures, use recursive mapped types carefully to avoid infinite recursion or performance issues.

Conclusion

Mapped types are a versatile feature in TypeScript that allow you to create new types by transforming existing ones. They are essential for creating reusable utilities, reducing redundancy, and improving the maintainability of your code.

By mastering mapped types, you can dynamically apply changes to object types, such as making properties optional, required, or read-only, as well as altering the types of properties. Whether you’re working with simple objects or deeply nested structures, mapped types provide the flexibility to adapt your types to your needs.

Conditional Types: Extends, Infer, and Distributive Conditions

0
typscript course
typscript course

Table of Contents

  • Introduction
  • What Are Conditional Types in TypeScript?
  • Basic Syntax of Conditional Types
  • Understanding extends in Conditional Types
  • The infer Keyword in Conditional Types
  • Distributive Conditional Types
  • Practical Examples
    • Example 1: Using extends in Conditional Types
    • Example 2: Using infer for Type Inference
    • Example 3: Distributive Conditional Types
  • Best Practices for Using Conditional Types
  • Conclusion

Introduction

Conditional types are one of the most powerful features in TypeScript, enabling developers to define types that depend on certain conditions. They allow you to conditionally assign a type based on another type, offering greater flexibility in type definitions.

Conditional types are expressed with the following syntax:

T extends U ? X : Y

Where:

  • T is the type being checked.
  • U is the type being tested against.
  • X is the result if the condition T extends U is true.
  • Y is the result if the condition T extends U is false.

Conditional types open up possibilities for dynamic and context-sensitive type assignments, enabling a more powerful and expressive type system.

This article will dive into the key components of conditional types, such as extends, infer, and distributive conditional types. Weโ€™ll explore each concept with detailed examples and show you how to use them effectively.


What Are Conditional Types in TypeScript?

Conditional types allow you to create types that depend on a condition. They are particularly useful when you need to define types that vary based on the structure or properties of other types.

For example, let’s say you want to create a type that is different depending on whether the provided type is a string or a number. With conditional types, you can express this as:

type MyType<T> = T extends string ? "String type" : "Other type";

Here, MyType will resolve to "String type" if the type passed in T is string, and "Other type" otherwise.


Basic Syntax of Conditional Types

The basic syntax for conditional types in TypeScript is:

T extends U ? X : Y
  • T: The type that is checked against the condition.
  • U: The type that is being tested.
  • X: The resulting type if the condition is true (T extends U).
  • Y: The resulting type if the condition is false (T extends U).

Example:

type IsString<T> = T extends string ? "Yes" : "No";

type Test1 = IsString<string>; // "Yes"
type Test2 = IsString<number>; // "No"

In this example:

  • IsString<string> resolves to "Yes" because string extends string.
  • IsString<number> resolves to "No" because number does not extend string.

Understanding extends in Conditional Types

The extends keyword plays a pivotal role in conditional types. It is used to check whether a given type extends (or is compatible with) another type. If the type extends, the first part of the conditional type (X) is chosen; otherwise, the second part (Y) is used.

Example: Using extends in Conditional Types

type IsArray<T> = T extends any[] ? "Array" : "Not an Array";

type Result1 = IsArray<number[]>; // "Array"
type Result2 = IsArray<string>; // "Not an Array"

Here:

  • IsArray<number[]> resolves to "Array" because number[] extends any[].
  • IsArray<string> resolves to "Not an Array" because string does not extend any[].

This type of conditional logic is extremely helpful when you need to differentiate between various types and provide tailored behavior accordingly.


The infer Keyword in Conditional Types

The infer keyword is another powerful feature that TypeScript provides for working with conditional types. It allows you to infer a type within a conditional expression.

Example: Using infer for Type Inference

type ReturnTypeOf<T> = T extends (...args: any[]) => infer R ? R : never;

type Func1 = ReturnTypeOf<() => string>; // string
type Func2 = ReturnTypeOf<(a: number) => void>; // void
type Func3 = ReturnTypeOf<() => number[]>; // number[]

Here:

  • ReturnTypeOf checks if the type T extends a function type. If it does, it infers the return type (R) of the function. Otherwise, it returns never.
  • In the case of Func1, the return type of the function is inferred as string.
  • In Func2, the return type is inferred as void because the function doesnโ€™t return anything.
  • Func3 returns a number[] since the function returns an array of numbers.

Why infer Is Powerful

The infer keyword enables TypeScript to dynamically capture types within a conditional block, making it extremely useful for type transformations or complex type inference.


Distributive Conditional Types

Distributive conditional types are a special case in TypeScript where conditional types distribute over union types. This means that if you pass a union type to a conditional type, TypeScript applies the condition to each member of the union.

Example: Distributive Conditional Types

type IsString<T> = T extends string ? "Yes" : "No";

type Test = IsString<string | number>; // "Yes" | "No"

Here:

  • IsString<string | number> is distributed over the union, meaning the type is evaluated for both string and number. This results in "Yes" | "No".

This behavior is highly useful when you need to create types that work flexibly with multiple possible types, as it allows TypeScript to apply conditions to each type individually.


Practical Examples

Example 1: Using extends in Conditional Types

type IsBoolean<T> = T extends boolean ? "Yes" : "No";

type Test1 = IsBoolean<boolean>; // "Yes"
type Test2 = IsBoolean<number>; // "No"

In this example:

  • IsBoolean<boolean> resolves to "Yes" because boolean extends boolean.
  • IsBoolean<number> resolves to "No" because number does not extend boolean.

Example 2: Using infer for Type Inference

type FunctionReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type Result1 = FunctionReturnType<() => number>; // number
type Result2 = FunctionReturnType<(x: string) => boolean>; // boolean

Here:

  • FunctionReturnType uses infer to capture the return type of the function passed as T.

Example 3: Distributive Conditional Types

type Flatten<T> = T extends (infer U)[] ? U : T;

type Test1 = Flatten<number[]>; // number
type Test2 = Flatten<string[]>; // string
type Test3 = Flatten<boolean>; // boolean

In this example:

  • The type Flatten takes a type T and checks if it is an array. If it is, it returns the element type (U) of the array; otherwise, it returns T itself.
  • Flatten<number[]> resolves to number.
  • Flatten<boolean> resolves to boolean.

Best Practices for Using Conditional Types

  1. Use extends for Type Checks: When you need to test whether a type extends another, extends is the go-to approach. It allows you to create flexible conditional types that adapt to the structure of the types you work with.
  2. Leverage infer for Type Inference: infer is useful when you want to extract types from more complex structures like functions. Use it to capture return types, parameter types, and other dynamic types.
  3. Distributive Conditional Types: When working with union types, be aware that TypeScript applies the conditional logic to each member of the union. This can be used to create powerful, type-safe utilities that handle multiple types flexibly.
  4. Keep Types Simple: While conditional types offer powerful features, try to keep your type logic simple and understandable. Complex conditional types can make code harder to maintain.
  5. Avoid Overuse of never: Be cautious when using never as a fallback in conditional types. It can sometimes lead to overly restrictive types, so use it only when you expect that a condition should never be true.

Conclusion

Conditional types in TypeScript are an incredibly powerful feature that provides flexibility and type safety when working with types that depend on other types. By understanding and mastering the use of extends, infer, and distributive conditional types, you can create more dynamic, reusable, and robust code.

Through real-world examples and best practices, we’ve demonstrated how these features allow you to build complex type logic, making your TypeScript applications more precise and type-safe.

As you continue to explore TypeScriptโ€™s advanced types, conditional types will become an essential part of your toolkit, enabling you to write more flexible and intelligent code.

Using keyof, typeof, and Lookup Types with Generics

0
typscript course
typscript course

Table of Contents

  • Introduction
  • What Are keyof, typeof, and Lookup Types?
  • Why Use keyof, typeof, and Lookup Types with Generics?
  • Understanding keyof with Generics
  • Using typeof with Generics
  • Lookup Types and How to Use Them with Generics
  • Practical Examples
    • Example 1: Using keyof with Generics
    • Example 2: Using typeof with Generics
    • Example 3: Lookup Types with Generics
  • Best Practices for Using keyof, typeof, and Lookup Types with Generics
  • Conclusion

Introduction

In TypeScript, generics offer flexibility and type safety when working with a wide variety of data types. However, there are some advanced techniques you can use in combination with generics to make your code even more powerful. These techniques include the keyof operator, the typeof operator, and Lookup Types.

Each of these features enhances the way you work with types in TypeScript, and when used with generics, they allow you to define more precise and flexible type constraints. This article will explore these concepts, demonstrate how they work together, and provide practical examples.


What Are keyof, typeof, and Lookup Types?

keyof Operator

The keyof keyword in TypeScript is used to get the type of the keys of an object type. Essentially, keyof extracts the keys of an object and returns a union type of all possible keys.

For example:

type Person = {
name: string;
age: number;
};

type PersonKeys = keyof Person; // "name" | "age"

typeof Operator

The typeof keyword in TypeScript is used to refer to the type of a variable or object. It’s useful when you want to capture the type of an object or variable without having to manually define its type.

For example:

const person = {
name: 'Alice',
age: 30
};

type PersonType = typeof person; // { name: string, age: number }

Lookup Types

A lookup type in TypeScript is a type that looks up the type of a property of another type. It allows you to access a specific propertyโ€™s type dynamically by using the key of the type.

For example:

type Person = {
name: string;
age: number;
};

type AgeType = Person['age']; // number

Why Use keyof, typeof, and Lookup Types with Generics?

Using these features with generics allows you to create flexible yet strongly typed APIs. They give you the ability to:

  1. Dynamically access types based on object keys.
  2. Create more precise constraints and mappings between types.
  3. Use the types of variables or object properties to create more reusable functions and classes.

When combined with generics, these techniques enhance the flexibility of your code while preserving type safety.


Understanding keyof with Generics

The keyof operator can be very powerful when working with generics, especially when you want to restrict the type of keys that can be used.

Example: Using keyof with Generics

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}

const person = {
name: 'Alice',
age: 30,
};

console.log(getProperty(person, 'name')); // Output: Alice
// console.log(getProperty(person, 'address')); // Error: Property 'address' does not exist

In this example:

  • The generic function getProperty accepts two parameters: obj (the object) and key (the key of the object).
  • K extends keyof T ensures that key must be a valid key of the type T.
  • T[K] ensures that the return type is the type of the value at the given key.

By using keyof, we enforce that the key passed to the function must be a valid property of the object, preventing runtime errors.


Using typeof with Generics

The typeof operator is often used with generics to capture the type of a variable or object and use that type as the constraint for the generic. This can be especially helpful when dealing with dynamic types.

Example: Using typeof with Generics

const person = {
name: 'Bob',
age: 45,
};

function printObject<T>(obj: T): void {
console.log(obj);
}

type PersonType = typeof person;

const personObject: PersonType = { name: 'Charlie', age: 50 };
printObject(personObject); // Output: { name: 'Charlie', age: 50 }

In this example:

  • typeof person is used to capture the type of the person object.
  • We then define a new object personObject that must adhere to the same type as person.
  • This allows us to ensure that personObject has the same structure as person.

The use of typeof allows us to create flexible functions that can work with any object but still preserve type safety based on the actual structure of the object.


Lookup Types and How to Use Them with Generics

Lookup types are a powerful feature in TypeScript, allowing you to get the type of a property dynamically. When used with generics, lookup types allow you to define functions and classes that can dynamically work with properties based on their key.

Example: Using Lookup Types with Generics

type Person = {
name: string;
age: number;
};

function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}

const person: Person = {
name: 'Alice',
age: 30,
};

const name = getValue(person, 'name'); // string
const age = getValue(person, 'age'); // number

In this example:

  • We define a type Person with two properties: name and age.
  • The function getValue takes an object of type T and a key of type K which extends keyof T.
  • The return type T[K] represents the value type associated with the key K.

By using lookup types, we ensure that the function will return the correct type for the given key, making the code flexible and type-safe.


Practical Examples

Example 1: Using keyof with Generics

function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}

const user = { name: 'John', age: 28 };

const name = getValue(user, 'name'); // string
const age = getValue(user, 'age'); // number

Example 2: Using typeof with Generics

const settings = {
theme: 'dark',
layout: 'grid',
};

type SettingsType = typeof settings;

function printSettings<T>(settings: T): void {
console.log(settings);
}

printSettings(settings); // Output: { theme: 'dark', layout: 'grid' }

Example 3: Lookup Types with Generics

type Product = {
id: number;
name: string;
price: number;
};

function getProductValue<T, K extends keyof T>(product: T, key: K): T[K] {
return product[key];
}

const product = { id: 1, name: 'Laptop', price: 1200 };

const productName = getProductValue(product, 'name'); // string
const productPrice = getProductValue(product, 'price'); // number

Best Practices for Using keyof, typeof, and Lookup Types with Generics

  1. Use keyof for Type Safety with Object Keys: When working with objects, always use keyof to ensure that you only pass valid object keys to functions or classes.
  2. Combine typeof with Generics for Flexible Object Handling: Use typeof to dynamically capture the type of an object and use that type within a generic function or class.
  3. Leverage Lookup Types for Accessing Specific Properties: Use lookup types when you need to access a property dynamically but want to ensure type safety.
  4. Use Constraints to Limit Generics: Apply constraints like keyof and typeof in generics to limit the possible types and ensure your code is as flexible and safe as possible.
  5. Avoid Overusing Lookup Types: While lookup types are powerful, they can lead to complexity in your code. Use them when the need arises but avoid unnecessary complexity.

Conclusion

By using the keyof, typeof, and Lookup Types in combination with generics, you unlock a whole new level of flexibility and type safety in TypeScript. These tools allow you to write highly reusable, type-safe code while still handling dynamic data structures.

Through examples and use cases, we’ve seen how these features help to access specific properties, enforce type constraints, and ensure that your code remains robust and flexible.

As you gain experience with TypeScript, these techniques will become essential tools in your toolbox, enabling you to handle more complex scenarios with ease while keeping your code clean and safe.