This is the second day of my participation in the November Gwen Challenge. Check out the details: the last Gwen Challenge 2021

In languages like C# and Java, generics can be used to create reusable components that can support multiple types of data. This allows users to use components with their own data types.

The key purpose of generics is to provide meaningful constraints between members: instance members of the class, methods of the class, function parameters, and function return values.

The genericHello World

If we want to define a function identity, the arg is of type string and the return value is arg itself. Without generics, the function would look like this:

function identity(arg: string) :string {
  return arg;
}
Copy the code

To support any type of parameter, we can use the type variable T, which is a special variable that only represents the type, not the value.

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

We call this version of the Identity function generic because it can be applied to multiple types.

Once we have defined a generic function, we can use it in two ways.

The first method passes all the arguments, including the type arguments:

let output = identity<string> ("Hello World");  // type of output will be 'Hello World'
Copy the code

The second approach takes advantage of type corollary — that is, the compiler automatically helps us determine the type of T based on the parameters passed in. Type inference helps us keep our code lean and readable.

let output = identity("Hello World");  // type of output will be 'Hello World'
Copy the code

Function arguments can be arrays of type variables T[]. The code is as follows:

function loggingIdentity<T> (arg: T[]) :T[] {
  console.log(arg.length);
  return arg;
}
Copy the code

We can also implement the above example with Array

:

function loggingIdentity<T> (arg: Array<T>) :Array<T> {
  console.log(arg.length);
  return arg;
}
Copy the code

The generic type

The type of a generic function is no different from that of a non-generic function, except that there is a type parameter T first, like a function declaration:

function identity<T> (arg: T) :T {
  return arg;
}
let myIdentity: <T>(arg: T) = > T = identity;
console.log(typeof myIdentity) // "function"
Copy the code

We can also use different generic parameter names, as long as they match in number and usage.

function identity<T> (arg: T) :T {
  return arg;
}
let myIdentity: <U>(arg: U) = > U = identity;
console.log(typeof myIdentity) // "function"
Copy the code

Use more than one generic parameter:

function identity<T.L> (name: T, len: L) :T.L] {
  return [name, len];
}
Copy the code

We can also define generic functions using object literals with call signatures:

function identity<T> (arg: T) :T {
  return arg;
}
let myIdentity: {<T>(arg: T): T} = identity;
Copy the code

This led us to write our first generic interface.

A generic interface

Let’s take the object literal from the above example as an interface:

function identity<T> (arg: T) :T {
  return arg;
}
interface GenericIdentityFn {
  <T>(arg: T): T;
}
let myIdentity: GenericIdentityFn = identity;
Copy the code

We might want to treat the generic parameter T as a parameter to the entire interface, so that other members of the interface can know the type of the parameter.

function identity<T> (arg: T) :T {
  return arg;
}
interface GenericIdentityFn<T> {
  (arg: T): T;
}
let myIdentity: GenericIdentityFn<number> = identity;
Copy the code

A generic class

In addition to generic interfaces, we can also create generic classes. Generic classes look much like generic interfaces. Generic classes use (<>) to enclose generic types after the class name.

A class has two parts: a static part and an instance part. A generic class refers to the type of the instance part, so static attributes of the class cannot use this generic type.

Note that generic enumerations and generic namespaces cannot be created.

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) = > T;
}
let myGenericNumber = new GenericNumber<number> (); myGenericNumber.zeroValue =0;
myGenericNumber.add = function(x, y) { return x + y; };

let stringNumeric = new GenericNumber<string> (); stringNumeric.zeroValue ="";
stringNumeric.add = function(x, y) { return x + y; };
console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));
Copy the code

The GenericNumber generic class is not limited to the number type, but can also use strings or other more complex types.

Generic constraint

Make sure properties exist

We define an interface to describe the constraints. Create an interface that contains the.length attribute and use this interface and the extends keyword to implement the constraint:

interface Lengthwise {
  length: number;
}
function loggingIdentity<T extends Lengthwise> (arg: T) :T {
  console.log(arg.length);  // Now we know it has a .length property, so no more error
  return arg;
}
Copy the code

Now the generic function is constrained, so it is no longer applicable to any type. We need to pass a value that matches the constraint type and must contain the required length attribute:

loggingIdentity(3);  // Error, number doesn't have a .length property
loggingIdentity({length: 10.value: 3});
Copy the code

Checks whether a key exists on an object

You can declare one type parameter and it is bound by another type parameter.

For example, now we want to get the property from the object using the property name, and we want to make sure the property exists on the object.

function getProperty<T.K extends keyof T> (obj: T, key: K) :T[K] {
  return obj[key];
}
let x = { a: 1.b: 2.c: 3.d: 4 };
getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.
Copy the code

A generic class

To use generics in a class, we simply add multiple type variables

.
,…>

interface MyInt<U> {
  value: U
  getValue: () = > U
}
class MyClass<T> implements MyInt<T> {
  value: T
  constructor(value: T) {
    this.value = value
  }
  getValue(): T {
    return this.value
  }
}
const myNumberClass = new MyClass<number> (111);
console.log(myNumberClass.getValue()); / / 111
const myStringClass = new MyClass<string> ("hhh");
console.log(myStringClass.getValue()); // hhh
Copy the code

Use generics to create objects

Construct the signature

In TypeScript interfaces, you can use the new keyword to describe a constructor:

interface Point {
  new (x: number.y: number): Point;
}
Copy the code

The new (x: number, y: number) in the above interface is called the construction signature, and its syntax is as follows:

ConstructSignature: newTypeParametersopt (ParameterListopt) TypeAnnotationopt// TypeParametersopt, ParameterListopt, and TypeAnnotationopt represent optional type parameters, an optional parameter list, and optional type annotations, respectively
Copy the code

Constructor type

In the TypeScript language specification, constructor types are defined as:

Object types that contain one or more construction signatures are called constructor types;

Constructor types can be written using constructor type literals or object type literals that contain construction signatures.

So what is a constructor type literal? Constructor type literals are shorthand for object types that contain the signature of a single constructor. In particular, constructor type literals take the form:

new < T1, T2, ... > ( p1, p2, ... ) = > R
Copy the code

This form is equivalent to the following object type literals:

{ new < T1, T2, ... > ( p1, p2, ... ) : R }
Copy the code

Example 1:

Constructor type literals:

// Constructor type literals
new (x: number.y: number) => Point
Copy the code

Equivalent to the following object type literals:

// Object type literals
{
  new (x: number.y: number): Point;
}
Copy the code

Example 2:

interface Point {
  x: number;
  y: number;
}
interface PointConstructor { // Constructor type
  new (x: number.y: number): Point;
}
// Class implements the interface
class Point2D implements Point {
  readonly x: number;
  readonly y: number;
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y; }}// Define the function newPoint
function newPoint(pointConstructor: PointConstructor, x: number, y: number) :Point {
  return new pointConstructor(x, y);
}
const point: Point = newPoint(Point2D, 2.2);
console.log(point);
Copy the code

Use generics to create objects

Sometimes, a generic class may need to create its type-related objects based on the passed generic T. Such as:

class FirstClass { id? :number;
}
class SecondClass {
  name: string;
}
class GenericCreator<T> {
  create<T>(c: { new (): T }): T {
    return newc(); }}const creator1 = new GenericCreator<FirstClass>();
const firstClass: FirstClass = creator1.create(FirstClass);
firstClass.id = 1;
const creator2 = new GenericCreator<SecondClass>();
const secondClass: SecondClass = creator2.create(SecondClass);
console.log(firstClass, secondClass)
Copy the code

In the code above, we defined two normal classes and a generic class GenericCreator

. In the generic GenericCreator generic class, we define a member method called create that uses the new keyword to call the constructor of the actual type passed in to create the corresponding object.

After writing the above code, SOMETIMES Typescript is too cumbersome… = =!

reference

  • Generic · TypeScript Chinese Language

  • Learn about TypeScript generics and applications

  • TypeScript generics you don’t know about