typescript泛型详解

和Java和C#相比,可以说typescript中的泛型更加复杂和灵活。

泛型Hello World

假设有下列恒等函数(identity function), 输入参数,返回参数:

//arg 只能是number类型,如果还想介绍其它类型,只能使用any
//如果使用any,实际上失去了类型约束
function identity(arg: number): number {
  return arg;
}

因此,我们需要一种捕获参数类型的方法,以便我们也可以使用它来表示返回的类型。

在这里,我们将使用类型变量,一种特殊的变量,它作用于类型而不是值。

//declare function identity<Type>(arg: Type): Type;
function identity<Type>(arg: Type): Type {
  return arg;
}
let output = identity<string>("myString");

即:添加了一个类型变量(type variable) Type, Type 可以捕获用户输入的参数(e.g. string),并且后续我们就可以使用这个类型Type, 例如将Type作为返回值的类型。

:zap: 以上,我们即为identity函数添加了泛型。

泛型的使用

当我们定义了上述的泛型函数后,有两种使用方式:

第一种方式

传入所有参数,包括类型参数(type argument):

let output = identity<string>("myString");
//生成类型如下:
declare function identity<Type>(arg: Type): Type;
declare let output: string;

可以看到,参数只能接受string类型,并且返回值,也识别为string。

这里我们明确的指定了Tstring类型,并做为一个参数传给函数,使用了<>括起来而不是()

第二种方法更普遍

利用了_类型推论_ (type argument inference )-- 即编译器会根据传入的参数自动地帮助我们确定T的类型:

let output = identity("myString");
//同样可以生成类型如下:
declare let output: string;

:zap: 注意我们没必要使用尖括号(<>)来明确地传入类型;

编译器可以查看myString的值,然后把T设置为它的类型。

使用泛型变量

使用泛型创建像identity这样的泛型函数时,编译器要求你在函数体必须正确的使用这个通用的类型。 换句话说,你必须把这些参数当做是任意或所有类型。

看,下面的代码,如果我们想同时打印出arg的长度:

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Error: T doesn't have .length
    return arg;
}

:notes: 记住,这些类型变量代表的是任意类型.

所以使用这个函数的人可能传入的是个数字,而数字是没有.length属性的。

现在假设我们想操作T类型的数组而不直接是T

由于我们操作的是数组,所以.length属性是应该存在的。 我们可以像创建其它数组一样创建这个数组:

function loggingIdentity<T>(arg: T[]): T[] {
  console.log(arg.length);  // 2
  return arg;
}
// 或者, 使用Array<T>, 同时返回类型可以省略
function loggingIdentityNew<T>(arg: Array<T>) {
  console.log(arg.length);  // 2
  return arg;
}

loggingIdentity([1,2])

泛型类型(Generic Types)

对于identity函数,我们使用泛型变量Type,约定了函数的参数和返回类型。

那如何定义函数本身的类型呢?

如果我们需要将identity函数赋值给myIdentity函数那如何定义myIdentity函数的类型呢?

我们可以尝试使用编辑器的类型推断看一下:

image-20220103153424908

因此,如果要添加myIdentity函数的类型,和普通变量的类型没有什么区别,只需在变量后添加类型即可:

function identity<T>(arg: T): T {
    return arg;
}
//<T>(arg: T) => T 即为函数的类型
let myIdentity: <T>(arg: T) => T = identity;
//T 可以使用不同的名称,如U
let myIdentity: <U>(arg: U) => U = identity;

:zap: 当然我们也可以使用typeof 获取类型

function identity<Type>(arg: Type): Type {
  return arg;
}
//type identityType = <Type>(arg: Type) => Type
type identityType = typeof identity
let myIdentity: identityType  = identity;

我们还可以使用带有调用签名的对象字面量来定义泛型函数:

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

let myIdentity: {<T>(arg: T): T} = identity;

泛型接口

既然可以使用对象字面量,同样可以使用接口:

interface GenericIdentityFn {
  <T>(arg: T): T;
}

function identity<Type>(arg: Type): Type {
  return arg;
}
 
let myIdentity: GenericIdentityFn = identity;

当然,我们也可以将泛型参数直接作为整个接口的参数,这样可以让我们更清楚的知道传入的是什么参数 (e.g.Dictionary<string> 而不是 Dictionary)。

interface GenericIdentityFn<Type> {
  (arg: Type): Type;
}
 
function identity<Type>(arg: Type): Type {
  return arg;
}
//当使用此接口类型时,需要明确给出参数的类型, 并且锁定了myIdentity调用签名的类型
let myIdentity: GenericIdentityFn<number> = identity;
myIdentity(123)  // OK
myIdentity("hello")  //Error

:zap: :tiger: 当要描述一个包含泛型的类型时,理解什么时候把类型参数放在调用签名里,什么时候把它放在接口里是很有用的。

泛型类(Generic Classes)

泛型类写法上类似于泛型接口。

在类名后面,使用尖括号中 <> 包裹住泛型类型参数列表(a generic type parameter list):

//注意:tsconfig.json 中应该配置:  "strictPropertyInitialization": false
class GenericNumber<NumType> {
  zeroValue: NumType;
  add: (x: NumType, y: NumType) => NumType;
}
 
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
  return x + y;
};

这就是泛型类最简单的使用方式。

使用泛型类,并不会限制NumType的类型,同样,你也可以使用string类型:

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

一个类它的类型有两部分:静态部分和实例部分。

泛型类仅仅对实例部分生效,所以当我们使用类的时候,注意静态成员并不能使用类型参数。

:zap: 更多泛型类示例:

class KeyValuePair<T,U>
{ 
    private key: T;
    private val: U;

    setKeyValue(key: T, val: U): void { 
        this.key = key;
        this.val = val;
    }

    display():void { 
        console.log(`Key = ${this.key}, val = ${this.val}`);
    }
}

let kvp1 = new KeyValuePair<number, string>();
kvp1.setKeyValue(1, "Steve");
kvp1.display(); //Output: Key = 1, Val = Steve 

let kvp2 = new KeyValuePair<string, string>();
kvp2.setKeyValue("CEO", "Bill"); 
kvp2.display(); //Output: Key = CEO, Val = Bill

泛型约束(Generic Constraints)

回到最初的例子:

function loggingIdentity<Type>(arg: Type): Type {
  console.log(arg.length); //Property 'length' does not exist on type 'Type'.
  return arg;
}

相比于能兼容任何类型,我们更愿意约束这个函数,让它只能使用带有 .length 属性的类型。

只要类型有这个成员,我们就允许使用它,但必须至少要有这个成员。

我们可以创建一个接口,用来描述约束。

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

现在,loggingIdentity泛型函数即被约束了,不在使用于所有类型,如:

loggingIdentity(3);
//Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.

//只有传入符合约束条件的才行,即至少有length属性
loggingIdentity({ length: 10, value: 3 });

在泛型约束中使用类型参数(Using Type Parameters in Generic Constraints)

举个例子:

我们希望获取一个对象给定属性名的值,为此,我们需要确保我们不会获取 obj 上不存在的属性。

所以我们在两个类型之间建立一个约束:

function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
  return obj[key];
}
 
let x = { a: 1, b: 2, c: 3, d: 4 };
 
getProperty(x, "a");
getProperty(x, "m");
Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.

在泛型中使用类类型(Using Class Types in Generics)

在 TypeScript 中,当使用工厂模式创建实例的时候,有必要通过他们的构造函数推断出类的类型.

:zap: e.g.

function create<Type>(c: { new (): Type }): Type {
  return new c();
}

下面是一个更复杂的例子,使用原型属性推断和约束,构造函数和类实例的关系。

class BeeKeeper {
  hasMask: boolean = true;
}
 
class ZooKeeper {
  nametag: string = "Mikle";
}
 
class Animal {
  numLegs: number = 4;
}
 
class Bee extends Animal {
  keeper: BeeKeeper = new BeeKeeper();
}
 
class Lion extends Animal {
  keeper: ZooKeeper = new ZooKeeper();
}
 
function createInstance<A extends Animal>(c: new () => A): A {
  return new c();
}
 
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;

泛型类实现泛型接口

interface IKeyValueProcessor<T, U>
{
    process(key: T, val: U): void;
};

class kvProcessor<T, U> implements IKeyValueProcessor<T, U>
{ 
    process(key:T, val:U):void { 
        console.log(`Key = ${key}, val = ${val}`);
    }
}

let proc: IKeyValueProcessor<number, string> = new kvProcessor();
proc.process(1, 'Bill'); //Output: key = 1, value = Bill 

为泛型类型设置默认类型

//在默认添加 = 指定默认类型,例如  = string,指定默认类型为string, = number指定默认类型为number
function makeState<S extends number | string = string>() {
  let state: S
  function getState() {
    return state
  }
  function setState(x: S) {
    state = x
  }
  return { getState, setState }
}
// S 默认为 string 类型
const strState = makeState()
strState.setState("hello")  //OK
strState.setState(123)  // Errors: Argument of type 'number' is not assignable to parameter of type 'string'.

常规函数和泛型函数的区别

What you should remember is that generics are just like regular function parameters.

The difference is that regular function parameters deal with values, but generics deal with type parameters.

泛型就像常规函数参数一样。 区别在于常规函数参数处理值,而泛型处理类型参数。

//1 .a regular function. Declare a regular function
function regularFunc(x: any) {
  // You can use x here
}
// Call it: x will be 1
regularFunc(1)

//2.a generic function with a type parameter.Declare a generic function
function genericFunc<T>() {
  // You can use T here
}
// Call it: T will be number
genericFunc<number>()

//3. 普通函数设置默认值
// Set the default value of x
function regularFunc(x = 2)
// x will be 2 inside the function
regularFunc()

//4.泛型函数设置默认类型
// Set the default type of T
function genericFunc<T = number>()
// T will be number inside the function
genericFunc()

泛型接口与类型别名

function makePair<F, S>() {
  let pair: { first: F; second: S }
  // ...
}

我们可以将字面量 { first: F, second: S } 提取到 interface or a type alias 中。

// Extract into a generic interface
// to make it reusable
interface Pair<A, B> {
  first: A
  second: B
}

// Extract into a generic type alias. It’s
// basically identical to using an interface
type Pair<A, B> = {
  first: A
  second: B
}


// 使用
function makePair<F, S>() {
  // Usage: Pass F for A and S for B
  let pair: Pair<F, S>
  // ...
}

区别:

Interfaces vs Types in TypeScript - Stack Overflow

TypeScript: Documentation - Everyday Types (typescriptlang.org)

  • 都可以用于对象和函数的签名
//接口
interface Point {
  x: number;
  y: number;
}

interface SetPoint {
  (x: number, y: number): void;
}

//类型别名
type Point = {
  x: number;
  y: number;
};

type SetPoint = (x: number, y: number) => void;
  • the type alias can also be used for other types such as primitives, unions, and tuples.
// primitive
type Name = string;

// object
type PartialPointX = { x: number; };
type PartialPointY = { y: number; };

// union
type PartialPoint = PartialPointX | PartialPointY;

// tuple
type Data = [number, string];
  • Extend 和 Implements
//Interface extends interface
interface PartialPointX { x: number; }
interface Point extends PartialPointX { y: number; }

//Type alias extends type alias
type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };

//Interface extends type alias
type PartialPointX = { x: number; };
interface Point extends PartialPointX { y: number; }

//Type alias extends interface
interface PartialPointX { x: number; }
type Point = PartialPointX & { y: number; };

//interface implements
interface Point {
  x: number;
  y: number;
}

class SomePoint implements Point {
  x = 1;
  y = 2;
}

// type implements
type Point2 = {
  x: number;
  y: number;
};

class SomePoint2 implements Point2 {
  x = 1;
  y = 2;
}

type PartialPoint = { x: number; } | { y: number; };

// Errors: FIXME: can not implement a union type
class SomePartialPoint implements PartialPoint {
  x = 1;
  y = 2;
}
  • 接口的多次声明可以合并
// These two declarations become:
// interface Point { x: number; y: number; }
interface Point { x: number; }
interface Point { y: number; }

const point: Point = { x: 1, y: 2 };

内置泛型工具类型