Easy to Master the Generics in TypeScript

Why do I need a generic type?

A major part of software engineering is building components that not only have well-defined and consistent APIs, but are also reusable. Components that are capable of working on the data of today as well as the data of tomorrow will give you the most flexible capabilities for building up large software systems.

In languages like C# and Java, one of the main tools in the toolbox for creating reusable components is generics, that is, being able to create a component that can work over a variety of types rather than a single one. This allows users to consume these components and use their own types.

The official explanation above is hard for beginners to understand.

I think beginners should first understand why they need Generics, and what problems it solves. Instead of reading this kind of argumentative definition.

Let’s look at an example like this one to get a feel for the problem solved by the generic type.

Define a print function that prints out the incoming parameter and returns it. The incoming parameter is of type string and the function returns string.

function print(arg:string):string {
console.log(arg)
return arg
}

Now the requirements have changed and I still need to print the number type, what can I do?

You can use union types to transform.

function print(arg:string | number):string | number {
console.log(arg)
return arg
}

Now the requirements have changed again, I also need to print string arrays, number arrays, or even any type, what should I do?

There is a stupid way to write as many union types as supported.

Or change the parameter type to any.

function print(arg:any):any {
console.log(arg)
return arg
}

Not to say that writing any type is bad, after all, try not to write any in TypeScirpt.

And this is not the result we want, we can only say that the incoming value is of type any and the outgoing value is of type any, and the incoming and returning values are not uniform.

There are even bugs in this way:

const res:string = print(123)

Defining a string type to receive the return value of the print function returns a number type, and TS does not report an error to us.

This is where generics come in, and they can easily solve the problem of consistent input and output.

Note: Generic is not designed to solve this one problem. Generic also solves many other problems, and this example is used here to draw out generic.

Basic Use of Generics

  1. Handling function parameters

The syntax of a generic type is to write the type parameter in <>, which can generally be represented as T.

function print<T>(arg:T):T {
console.log(arg)
return arg
}

In this way, we have achieved a uniform type of input and output, and can input and output any type.

The T in a generic type is like a placeholder, or a variable, that can be passed in like a parameter when used with the defined type, and it can be output as is.

The way generics are written is a bit odd for front-end engineers, like <> T, but it’s good to remember that as soon as you see <>, you know it’s a generic.

There are two ways we can specify the type when we use it.

  • Define the type to be used
  • TS Type inference to automatically derive the type
print<string>('hello')  // Define T as string
print('hello')
// TS type inference, automatic derivation of the type string

As we know, both type and interface can define function types, so let’s also write them in generic terms.

type Print = <T>(arg: T) => T
const printFn:Print = function print(arg) {
console.log(arg)
return arg
}

interface written like this.

interface Iprint<T> {
(arg: T): T
}
function print<T>(arg:T) {
console.log(arg)
return arg
}
const myPrint: Iprint<number> = print

2. Default Parameters

If you want to add default parameters to a generic type, you can write it like this.

interface Iprint<T = number> {
(arg: T): T
}
function print<T>(arg:T) {
console.log(arg)
return arg
}
const myPrint: Iprint = print

So the default is the number type, how about it, does it feel like T is like a function parameter?

3. Handling multiple function parameters

Now there is a function that passes in a tuple with only two items, swaps item 0 and item 1 of the tuple, and returns the tuple.

function swap(tuple) {
return [tuple[1], tuple[0]]
}

Writing it this way, we lose the type and transform it a bit with a generic type.

We use T to represent the type of item 0 and U to represent the type of item 1.

function swap<T, U>(tuple: [T, U]): [U, T]{
return [tuple[1], tuple[0]]
}

This enables the control of tuple item 0 and item 1 types.

4. Function side effect operations

Generics are not only handy for constraining the types of arguments to functions, but also for functions that perform side effect operations.

For example, we have a generic asynchronous request method that wants to return different types of data based on different url requests.

function request(url:string) {
return fetch(url).then(res => res.json())
}

Call an interface to obtain information about the user.

request('user/info').then(res =>{
console.log(res)
})

The return result res is an any type, which is very annoying.

We want the API calls to be clear about what data structure the return type is, so we can do this.

interface UserInfo {
name: string
age: number
}
function request<T>(url:string): Promise<T> {
return fetch(url).then(res => res.json())
}
request<UserInfo>('user/info').then(res =>{
console.log(res)
})

This makes it very comfortable to get the data type returned by the interface and makes development much more efficient.

Constraint Generics

Suppose now there is such a function that prints the length of the incoming parameters, and we write it like this.

function printLength<T>(arg: T): T {
console.log(arg.length)
return arg
}

Because it is not certain that T has a length attribute, an error is reported.

So now I want to constrain the generic type, which must have a length attribute, what should I do?

You can combine it with interface to constrain the type.

interface ILength {
length: number
}

function printLength<T extends ILength>(arg: T): T {
console.log(arg.length)
return arg
}

The key to this is <T extends ILength>, which allows the generic to inherit from the interface ILength so that it can constrain the generic.

The variables we define must have length attributes, such as str, arr and obj below, to pass TS compilation.

const str = printLength('lin')
const arr = printLength([1,2,3])
const obj = printLength({ length: 10 })

This example also reaffirms the duck typing of interfaces.

As long as you have the length attribute, you’re in compliance with the constraint, so it doesn’t matter if you’re str, arr, or obj.

Of course, if we define a variable that does not contain a length attribute, such as a number, it will report an error:

Some applications of generics

With generic types, you can define functions, interfaces, or classes without specifying a specific type in advance, but instead specify the type at the time of use.

  1. Generic Constraint Class

Define a stack with two methods, in-stack and out-stack. If you want the in-stack and out-stack elements to be of uniform type, you can write it like this.

class Stack<T> {
private data: T[] = []
push(item:T) {
return this.data.push(item)
}
pop():T | undefined {
return this.data.pop()
}
}

When defining the instance, write the type, for example, if the in-stack and out-stack are both of type number, then write this.

const s1 = new Stack<number>()

In this way, stacking a string will report an error.

This is very flexible, if the requirements change and the incoming and outgoing stacks are of string type, just change it when defining the instance.

const s1 = new Stack<string>()

In particular, note that generics cannot constrain static members of a class.

Defining the static keyword for the pop method reports an error:

2. Generic Constraint Interface

Using generics, you can also adapt the interface to make it more flexible.

interface IKeyValue<T, U> {
key: T
value: U
}

const k1:IKeyValue<number, string> = { key: 18, value: 'lin'}
const k2:IKeyValue<string, number> = { key: 'lin', value: 18}

3. Generic Definition Arrays

Define an array, as we wrote before.

const arr: number[] = [1,2,3]

Now it is also possible to write this.

const arr: Array<number> = [1,2,3]

Wrong type of array item, error reported

Practical, generic constraints on back-end interface parameter types

Let’s look at a usage of generic types that is very helpful for project development, constraining back-end interface parameter types.

import axios from 'axios'

interface API {
'/book/detail': {
id: number,
},
'/book/comment': {
id: number
comment: string
}
...
}


function request<T extends keyof API>(url: T, obj: API[T]) {
return axios.post(url, obj)
}

request('/book/comment', {
id: 1,
comment: 'great!'
})

This way there will be a reminder when the interface is called, e.g.

Wrong path is written:

The wrong parameter type was passed:

Parameters passed less than:

Summary

Generics, taken literally, are general and broad.
A generic type is a function, interface, or class that is defined without specifying a specific type in advance, but instead specifies the type at the time of use.
T in a generic type is like a placeholder, or a variable, that can be passed in the defined type like a parameter at the time of use, and it can be output as is.
Generic type provides meaningful constraints between members, which can be: function parameters, function return values, instance members of a class, methods of a class, etc.

If you are interested in my articles, you can follow me on Medium or Twitter.


Easy to Master the Generics in TypeScript was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.

Why do I need a generic type?

A major part of software engineering is building components that not only have well-defined and consistent APIs, but are also reusable. Components that are capable of working on the data of today as well as the data of tomorrow will give you the most flexible capabilities for building up large software systems.

In languages like C# and Java, one of the main tools in the toolbox for creating reusable components is generics, that is, being able to create a component that can work over a variety of types rather than a single one. This allows users to consume these components and use their own types.

The official explanation above is hard for beginners to understand.

I think beginners should first understand why they need Generics, and what problems it solves. Instead of reading this kind of argumentative definition.

Let’s look at an example like this one to get a feel for the problem solved by the generic type.

Define a print function that prints out the incoming parameter and returns it. The incoming parameter is of type string and the function returns string.

function print(arg:string):string {
console.log(arg)
return arg
}

Now the requirements have changed and I still need to print the number type, what can I do?

You can use union types to transform.

function print(arg:string | number):string | number {
console.log(arg)
return arg
}

Now the requirements have changed again, I also need to print string arrays, number arrays, or even any type, what should I do?

There is a stupid way to write as many union types as supported.

Or change the parameter type to any.

function print(arg:any):any {
console.log(arg)
return arg
}

Not to say that writing any type is bad, after all, try not to write any in TypeScirpt.

And this is not the result we want, we can only say that the incoming value is of type any and the outgoing value is of type any, and the incoming and returning values are not uniform.

There are even bugs in this way:

const res:string = print(123)

Defining a string type to receive the return value of the print function returns a number type, and TS does not report an error to us.

This is where generics come in, and they can easily solve the problem of consistent input and output.

Note: Generic is not designed to solve this one problem. Generic also solves many other problems, and this example is used here to draw out generic.

Basic Use of Generics

  1. Handling function parameters

The syntax of a generic type is to write the type parameter in <>, which can generally be represented as T.

function print<T>(arg:T):T {
console.log(arg)
return arg
}

In this way, we have achieved a uniform type of input and output, and can input and output any type.

The T in a generic type is like a placeholder, or a variable, that can be passed in like a parameter when used with the defined type, and it can be output as is.

The way generics are written is a bit odd for front-end engineers, like <> T, but it’s good to remember that as soon as you see <>, you know it’s a generic.

There are two ways we can specify the type when we use it.

  • Define the type to be used
  • TS Type inference to automatically derive the type
print<string>('hello')  // Define T as string
print('hello')
// TS type inference, automatic derivation of the type string

As we know, both type and interface can define function types, so let’s also write them in generic terms.

type Print = <T>(arg: T) => T
const printFn:Print = function print(arg) {
console.log(arg)
return arg
}

interface written like this.

interface Iprint<T> {
(arg: T): T
}
function print<T>(arg:T) {
console.log(arg)
return arg
}
const myPrint: Iprint<number> = print

2. Default Parameters

If you want to add default parameters to a generic type, you can write it like this.

interface Iprint<T = number> {
(arg: T): T
}
function print<T>(arg:T) {
console.log(arg)
return arg
}
const myPrint: Iprint = print

So the default is the number type, how about it, does it feel like T is like a function parameter?

3. Handling multiple function parameters

Now there is a function that passes in a tuple with only two items, swaps item 0 and item 1 of the tuple, and returns the tuple.

function swap(tuple) {
return [tuple[1], tuple[0]]
}

Writing it this way, we lose the type and transform it a bit with a generic type.

We use T to represent the type of item 0 and U to represent the type of item 1.

function swap<T, U>(tuple: [T, U]): [U, T]{
return [tuple[1], tuple[0]]
}

This enables the control of tuple item 0 and item 1 types.

4. Function side effect operations

Generics are not only handy for constraining the types of arguments to functions, but also for functions that perform side effect operations.

For example, we have a generic asynchronous request method that wants to return different types of data based on different url requests.

function request(url:string) {
return fetch(url).then(res => res.json())
}

Call an interface to obtain information about the user.

request('user/info').then(res =>{
console.log(res)
})

The return result res is an any type, which is very annoying.

We want the API calls to be clear about what data structure the return type is, so we can do this.

interface UserInfo {
name: string
age: number
}
function request<T>(url:string): Promise<T> {
return fetch(url).then(res => res.json())
}
request<UserInfo>('user/info').then(res =>{
console.log(res)
})

This makes it very comfortable to get the data type returned by the interface and makes development much more efficient.

Constraint Generics

Suppose now there is such a function that prints the length of the incoming parameters, and we write it like this.

function printLength<T>(arg: T): T {
console.log(arg.length)
return arg
}

Because it is not certain that T has a length attribute, an error is reported.

So now I want to constrain the generic type, which must have a length attribute, what should I do?

You can combine it with interface to constrain the type.

interface ILength {
length: number
}

function printLength<T extends ILength>(arg: T): T {
console.log(arg.length)
return arg
}

The key to this is <T extends ILength>, which allows the generic to inherit from the interface ILength so that it can constrain the generic.

The variables we define must have length attributes, such as str, arr and obj below, to pass TS compilation.

const str = printLength('lin')
const arr = printLength([1,2,3])
const obj = printLength({ length: 10 })

This example also reaffirms the duck typing of interfaces.

As long as you have the length attribute, you’re in compliance with the constraint, so it doesn’t matter if you’re str, arr, or obj.

Of course, if we define a variable that does not contain a length attribute, such as a number, it will report an error:

Some applications of generics

With generic types, you can define functions, interfaces, or classes without specifying a specific type in advance, but instead specify the type at the time of use.

  1. Generic Constraint Class

Define a stack with two methods, in-stack and out-stack. If you want the in-stack and out-stack elements to be of uniform type, you can write it like this.

class Stack<T> {
private data: T[] = []
push(item:T) {
return this.data.push(item)
}
pop():T | undefined {
return this.data.pop()
}
}

When defining the instance, write the type, for example, if the in-stack and out-stack are both of type number, then write this.

const s1 = new Stack<number>()

In this way, stacking a string will report an error.

This is very flexible, if the requirements change and the incoming and outgoing stacks are of string type, just change it when defining the instance.

const s1 = new Stack<string>()

In particular, note that generics cannot constrain static members of a class.

Defining the static keyword for the pop method reports an error:

2. Generic Constraint Interface

Using generics, you can also adapt the interface to make it more flexible.

interface IKeyValue<T, U> {
key: T
value: U
}

const k1:IKeyValue<number, string> = { key: 18, value: 'lin'}
const k2:IKeyValue<string, number> = { key: 'lin', value: 18}

3. Generic Definition Arrays

Define an array, as we wrote before.

const arr: number[] = [1,2,3]

Now it is also possible to write this.

const arr: Array<number> = [1,2,3]

Wrong type of array item, error reported

Practical, generic constraints on back-end interface parameter types

Let’s look at a usage of generic types that is very helpful for project development, constraining back-end interface parameter types.

import axios from 'axios'

interface API {
'/book/detail': {
id: number,
},
'/book/comment': {
id: number
comment: string
}
...
}


function request<T extends keyof API>(url: T, obj: API[T]) {
return axios.post(url, obj)
}

request('/book/comment', {
id: 1,
comment: 'great!'
})

This way there will be a reminder when the interface is called, e.g.

Wrong path is written:

The wrong parameter type was passed:

Parameters passed less than:

Summary

Generics, taken literally, are general and broad.
A generic type is a function, interface, or class that is defined without specifying a specific type in advance, but instead specifies the type at the time of use.
T in a generic type is like a placeholder, or a variable, that can be passed in the defined type like a parameter at the time of use, and it can be output as is.
Generic type provides meaningful constraints between members, which can be: function parameters, function return values, instance members of a class, methods of a class, etc.

If you are interested in my articles, you can follow me on Medium or Twitter.


Easy to Master the Generics in TypeScript was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.


Print Share Comment Cite Upload Translate
APA
Maxwell | Sciencx (2024-03-29T05:49:00+00:00) » Easy to Master the Generics in TypeScript. Retrieved from https://www.scien.cx/2022/11/14/easy-to-master-the-generics-in-typescript/.
MLA
" » Easy to Master the Generics in TypeScript." Maxwell | Sciencx - Monday November 14, 2022, https://www.scien.cx/2022/11/14/easy-to-master-the-generics-in-typescript/
HARVARD
Maxwell | Sciencx Monday November 14, 2022 » Easy to Master the Generics in TypeScript., viewed 2024-03-29T05:49:00+00:00,<https://www.scien.cx/2022/11/14/easy-to-master-the-generics-in-typescript/>
VANCOUVER
Maxwell | Sciencx - » Easy to Master the Generics in TypeScript. [Internet]. [Accessed 2024-03-29T05:49:00+00:00]. Available from: https://www.scien.cx/2022/11/14/easy-to-master-the-generics-in-typescript/
CHICAGO
" » Easy to Master the Generics in TypeScript." Maxwell | Sciencx - Accessed 2024-03-29T05:49:00+00:00. https://www.scien.cx/2022/11/14/easy-to-master-the-generics-in-typescript/
IEEE
" » Easy to Master the Generics in TypeScript." Maxwell | Sciencx [Online]. Available: https://www.scien.cx/2022/11/14/easy-to-master-the-generics-in-typescript/. [Accessed: 2024-03-29T05:49:00+00:00]
rf:citation
» Easy to Master the Generics in TypeScript | Maxwell | Sciencx | https://www.scien.cx/2022/11/14/easy-to-master-the-generics-in-typescript/ | 2024-03-29T05:49:00+00:00
https://github.com/addpipe/simple-recorderjs-demo