TypeScript 类、泛型的使用实践记录|青训营笔记

46 阅读6分钟

TypeScript 介绍

  1. TypeScript 是 JavaScript 的超集,提供了 JavaScript 的所有功能,并提供了可选的静态类型、Mixin、类、接口和泛型等特性。
  2. TypeScript 的目标是通过其类型系统帮助及早发现错误并提高 JavaScript 开发效率。
  3. 通过 TypeScript 编译器或 Babel 转码器转译为 JavaScript 代码,可运行在任何浏览器,任何操作系统。
  4. 任何现有的 JavaScript 程序都可以运行在 TypeScript 环境中,并只对其中的 TypeScript 代码进行编译。
  5. 在完整保留 JavaScript 运行时行为的基础上,通过引入静态类型定义来提高代码的可维护性,减少可能出现的 bug。
  6. 永远不会改变 JavaScript 代码的运行时行为,例如数字除以零等于 Infinity。这意味着,如果将代码从 JavaScript 迁移到 TypeScript ,即使 TypeScript 认为代码有类型错误,也可以保证以相同的方式运行。
  7. 对 JavaScript 类型进行了扩展,增加了例如 anyunknownnevervoid
  8. 一旦 TypeScript 的编译器完成了检查代码的工作,它就会 擦除 类型以生成最终的“已编译”代码。这意味着一旦代码被编译,生成的普通 JS 代码便没有类型信息。这也意味着 TypeScript 绝不会根据它推断的类型更改程序的 行为。最重要的是,尽管可能会在编译过程中看到类型错误,但类型系统自身与程序如何运行无关。
  9. 在较大型的项目中,可以在单独的文件 tsconfig.json 中声明 TypeScript 编译器的配置,并细化地调整其工作方式、严格程度、以及将编译后的文件存储在何处。

泛型

泛型是一种捕获参数类型的方法,用来创建能够在多种类型上工作可重用的组件,而不是单个类型,这样用户就可以以自己的数据类型来使用组件。

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

这里,使用了一个类型变量 T,它是一种特殊的变量,只用于表示类型而不是值。T 帮助捕获用户传入的类型(比如:number),之后就可以使用这个类型。再次使用了 T 当做返回值类型,这样参数类型与返回值类型就是相同的了。

可以用两种方式调用一个泛型函数:

  1. 第一种方式是将所有参数(包括类型参数)传递给函数。
let output = identity<string>("myString");
// let output: string

这里,显式地将 T 设置为 string,使用了 <> 括起来,并作为函数调用的参数之一。

  1. 第二种方式是最常见的,使用类型参数推断,编译器根据传入的参数类型自动设置 T 的类型。
let output2 = identity("myString");
// let output2: string

不必在尖括号(<>)中显式传递类型,编译器只是根据值 myString,即可将 T 设置为其类型。虽然类型参数推断是保持代码更短、更可读的有用工具,但当编译器无法推断类型时,比如在一些复杂的情况下,还是需要像第一种方式那样显式传递类型参数。

泛型变量

泛型变量代表的是任意类型。例如要在一个函数中,打印一个参数的长度。由于使用这个函数的人可能传入的是个数字,而数字是没有 length 属性的,所以会报错。

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

但如果操作的是 T 类型的数组,length 属性是存在的。

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

泛型函数 loggingIdentity 接收泛型参数 T 和类型是 T[] 的数组参数 arg,并返回类型是 T[] 的数组。

泛型类型

  1. 可以使用不同的泛型参数名,只要在数量上和使用方式上能对应上就可以。
let myIdentity: <Input>(arg: Input) => Input = identity;
  1. 还可以使用带有调用签名的对象字面量类型来定义泛型函数。
let myIdentity2: { <T>(arg: T): T } = identity;
  1. 可以把上面例子里的对象字面量拿出来做为一个泛型接口。
interface GenericIdentityFn {
  <T>(arg: T): T;
}
function identity<T>(arg: T): T {
  return arg;
}
let myIdentity: GenericIdentityFn = identity;
  1. 可以把泛型参数也当作整个接口的一个参数,就能清楚的知道使用的具体是哪个泛型类型,接口里的其它成员也能知道这个参数的类型了。
interface GenericIdentityFn<T> {
  (arg: T): T;
}
function identity<T>(arg: T): T {
  return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
let myIdentity2: GenericIdentityFn<string> = identity;

现在接口上有了一个非泛型函数签名,它是泛型类型的一部分,而不是描述泛型函数。当使用 GenericIdentityFn 时,还需要指定相应的类型参数(这里:number),从而有效地锁定了之后代码里使用的类型。了解何时将类型参数直接放在调用签名上和接口本身上,将有助于描述类型的哪些方面是属于泛型的。

除了泛型接口,还可以创建泛型类。但是,无法创建泛型枚举和泛型命名空间

泛型类

泛型类与泛型接口相似,在类名称后面的尖括号(<>)中有一个泛型类型参数列表。与接口一样,将类型参数放在类本身可以确保类的所有成员都使用同一类型。

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"));

类有两部分构成:静态部分和实例部分,而泛型类指的是实例部分的类型,所以类的静态成员不能使用类的泛型类型。

泛型约束

有时候想操作某类型的一组值,并且知道这组值具有什么样的属性。 例如在 loggingIdentity 例子中,想访问 arg 的 length 属性,但是编译器并不能证明每种类型都有 length 属性,所以就报错了。

如果希望只要该类型具有此成员,就允许使用它,需要创建一个包含 length 属性的接口,然后使用 extends 关键字后跟该接口,即可实现约束:

interface Lengthwise {
  length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

由于泛型函数现在受到约束,它不再适用于任何类型:

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

需要传入符合约束类型的值,包含所有必需的属性:

loggingIdentity({length: 10, value: 3});

在泛型约束中使用类型参数

声明一个泛型参数,且它被另一个泛型参数约束。下面的泛型参数 Key 被约束为参数 obj 对象中存在的属性:

function getProperty<T, Key extends keyof T>(obj: T, 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"'.

在泛型里使用类类型

使用泛型创建工厂函数时,需要通过其构造函数引用类类型。

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

更高级的用法是使用原型属性来推断并约束构造函数与类实例的关系,例如 Mixins 即使用了此模式。

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;