Skip to main content

generics-introduction-and-generic-functions

11. Generics: Introduction and Generic Functions

- Introduce the concept of generics to create reusable components and start with how to make functions generic.

Certainly! Generics are a cornerstone of TypeScript's type system, enabling you to write flexible and reusable code. Here are 20 key points about generics, focusing particularly on generic functions:

Generics in TypeScript:

  1. What Are Generics:

    Generics let you write a component or function that can work over a variety of types rather than a single one. This adds flexibility to your code.

function identity<T>(arg: T): T {
return arg;
}
  1. Basic Syntax:

    To create a generic function, you use angle brackets <> with a type variable, like <T>, which can then be used as a placeholder for a type.

function genericFunction<T>(arg: T): T {
return arg;
}
  1. Basic Generic Functions:

    A basic generic function uses a type variable in place of a specific type for parameters and return types.

function wrapInArray<T>(value: T): T[] {
return [value];
}
  1. Type Constraints:

    You can define constraints on generics to specify that the type variable must have certain properties.

function loggingIdentity<T extends { length: number }>(arg: T): T {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
  1. Default Type Arguments:

    Generics can have default types that are used when no explicit type is provided.

function createArray<T = string>(length: number, value: T): T[] {
return new Array(length).fill(value);
}
  1. Using Multiple Type Variables:

    You can use multiple type variables to define a function that interacts with multiple types.

function merge<U, V>(obj1: U, obj2: V): U & V {
return { ...obj1, ...obj2 };
}
  1. Generic Functions and Type Inference:

    TypeScript can often infer the type of the generic parameter from the arguments of the function.

let result = merge({ name: 'John' }, { age: 30 }); // TypeScript infers U to be { name: string } and V to be { age: number }
  1. Generic Function Overloads:

    Function overloads allow you to declare multiple function signatures for a single function name, each with different type parameters.

function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
if (d !== undefined && y !== undefined) {
return new Date(y, mOrTimestamp, d);
} else {
return new Date(mOrTimestamp);
}
}
  1. Generics with Arrow Functions:

    Arrow functions can also use generics. The generic type parameter comes before the arrow.

const logAndReturn = <T>(arg: T): T => {
console.log(arg);
return arg;
};
  1. Using Generics in Callbacks:

    When defining callback functions, you can use generics to allow the callback to work with any type.

function withCallback<T>(value: T, callback: (arg: T) => void): void {
callback(value);
}

withCallback(5, (n) => console.log(n.toFixed(2))); // Callback is a generic function that knows 'n' is a number
  1. Built-In Generic Types:

    TypeScript has built-in generic types like Array<T> or Promise<T> that demonstrate the utility and necessity of generics.

let list: Array<number> = [1, 2, 3];

let promise: Promise<string> = new Promise((resolve, reject) => {
resolve("This will return a string");
});
  1. Generic Utility Types:

    TypeScript offers built-in utility types that are generic, such as Partial<T>, Readonly<T>, and others, which modify types in specific ways.

interface Todo {
title: string;
description: string;
}

const todo: Partial<Todo> = {
title: "Finish TypeScript course"
};

const readonlyTodo: Readonly<Todo> = {
title: "Read TypeScript documentation",
description: "Get a deeper understanding of generics"
};
  1. Type Parameters in Return Types:

    The type parameter of a generic function can be used to specify its return type, offering more flexibility in function composition.

function identity<T>(arg: T): T {
return arg;
}

let output = identity("myString");
  1. Generic Rest Parameters:

    Generics can be applied to function parameters that are rest parameters, allowing for type-safe manipulation of argument lists.

function concatenate<T>(...args: T[]): string {
return args.join('');
}

let result = concatenate<string>('Hello', 'Generic', 'World');
  1. Generic Conditional Types:

    Using conditional types with generics can help create more dynamic and adaptable APIs.

type Check<T> = T extends string ? 'String' : 'Not String';
type Type = Check<string>; // Type is 'String'
  1. Generic Mapped Types:

    Mapped types can also use generic types to transform one type into another in a generic fashion.

type Properties = 'propA' | 'propB';
type MyMappedType<T> = {
[P in Properties]: T
};
type NewType = MyMappedType<boolean>;
  1. Generic Type Guards:

    Generics can be used in user-defined type guards to create dynamic checks that preserve type information.

function isString<T>(arg: T): arg is T & string {
return typeof arg === 'string';
}

if (isString(myVar)) {
console.log("It's a string!");
}
  1. Generic Decorators:

    You can create decorators that use generic types, useful for meta-programming tasks that need to be type-aware.

function log<T>(target: T) {
// A generic decorator example
}

@log
class MyClass { }
  1. Generics in Third-Party Libraries:

    Many third-party libraries use generics to allow for flexibility while maintaining type safety, offering real-world examples of best practices.

// For example, using generics with a third-party library like Axios for HTTP requests:
axios.get<User>('/user/12345').then(response => {
const user: User = response.data;
});
  1. Generics Pitfalls and Best Practices:

    Despite their utility, generics can be misused. Learn about common pitfalls, such as overly complex types, and best practices for keeping your generics readable and maintainable.

// It's a best practice to use clear and descriptive names for generic type parameters:
function firstElement<Type>(arr: Type[]): Type | undefined {
return arr[0];
}

// Instead of:
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}