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:
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;
}
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;
}
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];
}
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;
}
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);
}
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 };
}
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 }
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);
}
}
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;
};
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
Built-In Generic Types:
TypeScript has built-in generic types like
Array<T>
orPromise<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");
});
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"
};
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");
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');
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'
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>;
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!");
}
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 { }
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;
});
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];
}