Typescript Default Types And Conditional Generics Complete Guide

 Last Update:2025-06-22T00:00:00     .NET School AI Teacher - SELECT ANY TEXT TO EXPLANATION.    9 mins read      Difficulty-Level: beginner

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

🔔 Note: Select your programming language to check or run code at

💻 Run Code Compiler

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.

You May Like This Related .NET Topic

Login to post a comment.