Typescript Default Types And Conditional Generics Complete Guide
Understanding the Core Concepts of TypeScript Default Types and Conditional Generics
TypeScript Default Types and Conditional Generics
Default Types
Default types in TypeScript allow you to specify default values for generic type parameters. This is particularly useful when you have a generic function or class that might not always require a specific type parameter to be provided.
Syntax:
function genericFunction<T = string>(arg: T): T {
return arg;
}
In the above example, the type parameter T
defaults to string
if no other type is specified.
Significance:
- Flexibility: Default types provide flexibility in how generic functions and classes can be used.
- Simplicity: They can simplify the usage of generics, especially in cases where the most common type can be inferred or used as a default.
- Clarity: By setting default types, you can make code more self-explanatory, indicating the intended use of a generic type parameter.
Practical Usage:
Here’s a more detailed example using a generic class:
class Container<T = number> {
private _value: T;
constructor(value: T) {
this._value = value;
}
public getValue(): T {
return this._value;
}
public setValue(value: T): void {
this._value = value;
}
}
const defaultContainer = new Container(); // T defaults to number
const stringContainer = new Container<string>("Hello"); // T is explicitly set to string
Conditional Generics
Conditional generics in TypeScript allow you to create types based on conditions applied to other types. This feature enables advanced type logic within the type system, making it possible to implement complex type relationships and transformations.
Syntax:
type Conditional<T> = T extends string ? string[] : number[];
Here, the Conditional
type is an array of strings if T
is a string; otherwise, it is an array of numbers.
Significance:
- Type Safety: Conditional types ensure type safety in scenarios where different operations should occur based on the type of an argument or variable.
- Advanced Type Logic: They allow for the implementation of advanced type logic within TypeScript, enabling sophisticated type relationships and transformations.
- Code Reusability: Conditional generics can lead to more modular and reusable code by encapsulating type logic that can be reused across different parts of an application.
Practical Usage:
Here’s an example of how conditional generics can be used to create a type-safe event handling system:
interface EventMap {
click: MouseEvent;
focus: FocusEvent;
// Add more event types as needed
}
type EventPayload<E extends keyof EventMap> = EventMap[E];
function handleEvent<T extends keyof EventMap>(type: T, payload: EventPayload<T>): void {
console.log(`Handling ${type} event with payload`, payload);
}
handleEvent("click", new MouseEvent("click")); // Correct usage
handleEvent("focus", new FocusEvent("focus")); // Correct usage
// Uncommenting the following line will cause a TypeScript error
// handleEvent("click", new FocusEvent("focus")); // Incorrect usage
In the above example, the EventPayload
type is conditionally determined based on the type
parameter, ensuring that the correct type of payload is passed to the handleEvent
function.
Conclusion
Online Code run
Step-by-Step Guide: How to Implement TypeScript Default Types and Conditional Generics
Part 1: Understanding TypeScript Default Types
What are Default Types?
Default types allow you to specify a fallback type if no type is provided when a generic type is instantiated. This is especially useful in making your code more flexible with sensible defaults.
Example 1: Basic Generic Function with a Default Type
Let's start with a simple example where we have a generic function returning an array of the specified type. If no type is provided, it defaults to any
.
function makeArray<T = any>(value: T, size: number): T[] {
const result: T[] = [];
for (let i = 0; i < size; i++) {
result.push(value);
}
return result;
}
Using the Function:
// Using explicitly specified T
const numbers = makeArray<number>(5, 3); // [5, 5, 5]
const strings = makeArray<string>("hello", 2); // ["hello", "hello"]
// Using default T (any)
const anyValues = makeArray(5, 3); // [5, 5, 5] - TypeScript infers T as `number`
const otherAnyValues = makeArray("world", 2); // ["world", "world"] - TypeScript infers T as `string`
const mixedDefault = makeArray(true, 4); // [true, true, true, true] - TypeScript infers T as `boolean`
Example 2: Class with Default Type
Let's define a class that can store an item which defaults to string
if no type is provided.
class Box<T = string> {
value: T;
constructor(value: T) {
this.value = value;
}
log(): void {
console.log(`Inside Box: ${this.value}`);
}
}
// Using explicitly specified T
const numberBox = new Box<number>(42);
numberBox.log(); // Inside Box: 42
// Using default T (string)
const defaultBox = new Box("hello");
defaultBox.log(); // Inside Box: hello
const booleanBox = new Box<boolean>(false);
booleanBox.log(); // Inside Box: false
Part 2: Understanding TypeScript Conditional Generics
What are Conditional Generics?
Conditional generics are a powerful feature of TypeScript that allows types to be conditionally inferred based on some logic. They use the extends
keyword and the ternary-like conditional operator to determine the resulting type.
Example 1: Simple Conditional Type
Suppose we want to create a type that returns true
if a certain type extends another type, otherwise it returns false
.
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // A will be true
type B = IsString<42>; // B will be false
type C = IsString<string>; // C will be true
type D = IsString<number>; // D will be false
Example 2: Conditional Type to Extract Specific Properties
Here, we define a type that checks if each property of an object is a number
or a non-number and creates a new type from these properties.
type NumberKeys<T> = {
[K in keyof T]: T[K] extends number ? K : never;
}[keyof T];
interface User {
name: string;
age: number;
height: number;
email: string;
}
type NumericUserProps = NumberKeys<User>; // 'age' | 'height'
type NonNumericUserProps = NumberKeys<{ [K in keyof User]: User[K] extends number ? never : K }>; // 'name' | 'email'
console.log(NumericUserProps); // 'age' | 'height'
console.log(NonNumericUserProps); // 'name' | 'email'
Example 3: More Complex Conditional Type with Functions
We define a type called ReturnType<T>
which conditionally extracts the return type of a function if the type parameter T
actually is a function.
type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : any;
function greet(name: string): string {
return `Hello, ${name}!`;
}
function add(x: number, y: number): number {
return x + y;
}
type GreetReturn = ReturnType<typeof greet>; // string
type AddReturn = ReturnType<typeof add>; // number
const a: GreetReturn = "Hello, Alice!"; // Valid
const b: AddReturn = 8; // Valid
const c: GreetReturn = 1234; // Error: Type 'number' is not assignable to type 'string'
const d: AddReturn = "World"; // Error: Type 'string' is not assignable to type 'number'
Example 4: Type Guard with Conditional Generics
A practical use case is creating a type guard that ensures a value is one of several possible types.
type CheckType<U, T> = U extends T ? U : never;
function checkType<T>(arg: unknown, checkFn: (x: any) => x is T): arg is T {
return checkFn(arg);
}
function isNumber(arg: any): arg is number {
return typeof arg === 'number';
}
let data: unknown = 123;
if (checkType<number>(data, isNumber)) {
console.log("Data is a number:", data * 2); // Valid - data is narrowed down to number
} else {
console.log("Data is not a number");
}
Part 3: Combining Default Types and Conditional Generics
Example 1: Flexible Return Type Based on Parameter Presence
Let's create a function that optionally accepts a value and returns an array. If no value is provided, the return type should default to any
. Otherwise, it uses conditional generics to infer the type.
function ensureArray<T = any>(value?: T): T extends undefined ? any[] : T[] {
if (typeof value === 'undefined') {
return [] as any[];
}
return Array.isArray(value) ? value : [value];
}
// Using default type (any[])
const noValueArray = ensureArray();
noValueArray.push(5); // Valid
noValueArray.push("text"); // Valid
// Using inferred type (number[])
const numberArray = ensureArray(123);
numberArray.push(456); // Valid
// numberArray.push("text"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'
// Using inferred type (string[])
const stringArray = ensureArray("hello");
stringArray.push("world"); // Valid
// stringArray.push(123); // Error: Argument of type 'number' is not assignable to parameter of type 'string'
Example 2: Mapping Object Properties Conditionally
This example defines a utility type that maps each property of an object to either its original type or null
, depending on a condition.
Top 10 Interview Questions & Answers on TypeScript Default Types and Conditional Generics
Top 10 Questions and Answers on TypeScript Default Types and Conditional Generics
1. What are Default Types in TypeScript and why are they important?
Answer: Default Types in TypeScript allow developers to specify default values for generic type parameters. If no type argument is provided, the default is used. This feature makes libraries more flexible and less error-prone by providing sensible defaults.
function createArray<T = number>(length: number, value: T): T[] {
return Array(length).fill(value);
}
// No type argument provided, defaults to number
let numbers = createArray(5, 1); // [1, 1, 1, 1, 1]
2. How are Conditional Types different from Utility Types in TypeScript?
Answer: While Utility Types like Partial
, Readonly
, Record
, etc., provide common transformations on types, Conditional Types allow the creation of types based on a conditional check between types themselves. Utility Types are pre-defined and simple, whereas Conditional Types offer more complex and customized type logic.
// Utility Type
type PartialUser = Partial<User>;
// Conditional Type
type IsString<T> = T extends string ? true : false;
3. What does it mean for a type to "Distribute" over unions in TypeScript?
Answer: When a generic type applies to a union type, it dissertates, applying the transformation to each member of the union. This is because conditional types distribute over union types unless wrapped in a tuple or a mapped type.
type Result = IsString<number | string>; // false | true
4. Can you explain the concept of "Exclude" and "Extract" utility types in TypeScript?
Answer: The Exclude
type allows creating a type by removing specific members from a union, whereas Extract
creates a union from members of another union that are assignable to the first one.
type Excluded = Exclude<string | number, number>; // string
type Extracted = Extract<string | number, string>; // string
5. How do Conditional Generics help in creating more flexible type systems?
Answer: Conditional Generics let you enforce stricter rules for your types based on conditional types, allowing you to define more precise and powerful type checks. This is ideal for creating types that adapt to complex logic or behaviors.
type ComponentProps<T extends string | object> = T extends string
? { text: T }
: { obj: T };
6. What is the difference between type guards and type predicates in TypeScript?
Answer: Type Guards narrow types inside code blocks using built-in ways (like in
, typeof
, instanceof
), whereas Type Predicates are user-defined or third-party functions that return a boolean indicating a type assertion.
// Type Guard
if (typeof value === "string") {
// value is string inside this block
}
// Type Predicate
function isString(value: any): value is string {
return typeof value === "string";
}
if (isString(value)) {
// value is string inside this block
}
7. How can you use infer
to extract types from generics in TypeScript?
Answer: The infer
keyword is used in conditional types to infer types from complex types, such as function return types, array elements, or promise resolutions.
type ReturnType<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R
: any;
8. What is Type Assertion within generics in TypeScript?
Answer: Type Assertion within generics allows you to explicitly declare a type for a generic parameter, making the function or type more predictable and forcing type safety.
function assertType<T>(value: any): asserts value is T { ... }
9. How can Type Inference fail in TypeScript generics, and how do you address this?
Answer: Type Inference can fail when TypeScript cannot accurately determine the expected type, especially with complex structures or insufficient type information. You can address this by providing explicit generic type arguments.
let list: Array<number>; // Explicit type
let list = [1, 2, 3]; // Works well, number[] inferred automatically
10. Can Default Types and Conditional Generics be used together to simplify complex type management?
Answer: Absolutely. Using Default Types and Conditional Generics together allows you to create flexible, powerful, and reusable type systems, handling various type relationships and constraints.
Login to post a comment.