这是我参与「第五届青训营 」伴学笔记创造活动的第 8 天

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 'Type'.
  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: { <Type>(arg: Type): Type } = identity;
  1. 能够把上面比方里的目标字面量拿出来做为一个泛型接口。
interface GenericIdentityFn {
  <Type>(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
  return arg;
}
let myIdentity: GenericIdentityFn = identity;
  1. 能够把泛型参数也当作整个接口的一个参数,就能清楚的知道运用的详细是哪个泛型类型,接口里的其它成员也能知道这个参数的类型了。
interface GenericIdentityFn<Type> {
  (arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
  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 比方中,咱们想拜访 arglength 特点,可是编译器并不能证明每种类型都有 length 特点,所以就报错了。咱们期望只需该类型具有此成员,咱们就答应运用它。

咱们需求创立一个包括.length 特点的接口,运用这个接口和 extends 关键字来实现束缚:

interface Lengthwise {
  length: number;
}
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
  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});

在泛型束缚中运用类型参数

能够声明一个类型参数,且它被另一个类型参数所束缚。比方,现在咱们想要用特点名从目标里获取这个特点,而且咱们想要保证这个特点存在于目标 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"'.

在泛型里运用类类型

运用泛型创立工厂函数时,需求经过其结构函数引用类类型。

function create<Type>(c: { new (): Type }): Type {
  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;

类型转化(断言)

在处理类型时,有时需求重写变量的类型,例如库供给了不正确的类型。强制转化便是重写类型的过程。

as

转化变量的一种简略办法是运用 as 关键字,这将直接更改给定变量的类型。

let x: unknown = 'hello';
console.log((x as string).length);

强制转化实际上不会改动变量内数据的类型,例如,以下代码没有按预期作业,由于变量 x 仍然是一个数字。

let x: unknown = 4;
console.log((x as string).length); // prints undefined since numbers don't have a length

TypeScript 仍然会尝试对类型转化进行类型查看,以避免看起来不正确的类型转化,例如,由于 TypeScript 知道在不转化数据的情况下将字符串转化为数字是没有意义的,因此下面将抛出类型过错:

console.log((4 as string).length); // Error: Conversion of type 'number' to type 'string' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.

<>

运用 <> 的作业原理与运用 as 转化相同。

let x: unknown = 'hello';
console.log((<string>x).length);

这种类型的转化不适用于 TSX,例如在处理 React 文件时。

强制转化

若要掩盖 TypeScript 在强制转化时或许引发的类型过错,先强制转化为 unknown 类型,然后再转化为目标类型。

let x = 1;
console.log(((x as unknown) as string).length); // x实际上不是一个字符串,因此它将回来undefined
console.log((<string><unknown>x2).length); // 同上

类型护卫

假如一个值是联合类型,咱们只能拜访此联合类型的一切类型里共有的成员。

interface IBird {
  fly();
  layEggs();
}
interface IFish {
  swim();
  layEggs();
}
class Bird implements IBird {
  fly(){}
  layEggs(){}
}
class Fish implements IFish {
  swim(){}
  layEggs(){}
}
function getSmallPet(): IFish | IBird {
  return Math.random() > 0.5 ? new Fish() : new Bird();
}
let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim();    // errors

这个比方里, Bird 具有一个 fly 成员。 咱们不能确认一个 Bird | Fish 类型的变量是否有 fly 办法。假如变量在运转时是 Fish 类型,那么调用 pet.fly() 就出错了。

联合类型适合于那些值能够为不同类型的情况,咱们只能拜访联合类型中共同拥有的成员。但当咱们想确切地了解是否为 Fish 时怎么办?JavaScript 里常用来区分两个或许值的办法是查看成员是否存在。

// 每一个成员拜访都会报错
if (pet.swim) {
  pet.swim();
} else if (pet.fly) {
  pet.fly();
}

为了让这段代码作业,咱们需求运用类型断言:

if ((<Fish>pet).swim) {
  (<Fish>pet).swim();
}
else {
  (<Bird>pet).fly();
}

自界说的类型维护

类型护卫便是一些表达式,它们会在运转时查看以保证在某个效果域里的类型。能够看到,上面咱们不得不多次运用类型断言,而经过类型护卫机制,咱们一旦查看过类型,就能在之后的每个分支里清楚地知道 pet 的类型了。要界说一个类型护卫,咱们只需简略地界说一个函数,它的回来值是一个类型谓词

function isFish(pet: Fish | Bird): pet is Fish {
  return (<Fish>pet).swim !== undefined;
}

类型谓词为 parameterName is Type 方式(例如 pet is Fish), parameterName 必须是来自于当前函数签名里的一个参数名。每逢运用一些变量调用 isFish 时,TypeScript 会将变量缩减为那个详细的类型,只需这个类型与变量的原始类型是兼容的。

// 'swim' 和 'fly' 调用都没有问题了
if (isFish(pet)) {
  pet.swim();
}
else {
  pet.fly();
}

TypeScript 不只知道在 if 分支里 petFish 类型;它还清楚在 else 分支里,一定是 Bird 类型。

typeof 类型护卫

假如像下面这样利用类型断言来写:

function isNumber(x: any): x is number {
  return typeof x === "number";
}
function isString(x: any): x is string {
  return typeof x === "string";
}
function padLeft(value: string, padding: string | number) {
  if (isNumber(padding)) {
    return Array(padding + 1).join(" ") + value;
  }
  if (isString(padding)) {
    return padding + value;
  }
  throw new Error(`Expected string or number, got '${padding}'.`);
}

这儿界说了每个函数来判别类型是否是对应原始类型,这太麻烦了。幸运的是,现在咱们不必将 typeof x === "number" 抽象成一个函数,由于 TypeScript 能够主动将它识别为一个类型护卫,也便是说咱们能够直接在内联代码块里查看类型。

function padLeft(value: string, padding: string | number) {
  if (typeof padding === "number") {
    return Array(padding + 1).join(" ") + value;
  }
  if (typeof padding === "string") {
    return padding + value;
  }
  throw new Error(`Expected string or number, got '${padding}'.`);
}

typeof 类型护卫只要两种方式能被识别:typeof v === "typename"typeof v !== "typename"typename 必须是 numberstringbooleansymbol。可是,TypeScript 不会阻止你与其它字符串比较,只是不会把那些表达式识别为类型护卫。

instanceof 类型护卫

instanceof 类型护卫是经过结构函数来缩小类型的一种方法。instanceof 的右侧要求是一个结构函数,TypeScript 将按照次序缩小为:

  1. 假如类型不为 any,将是函数的 prototype 类型。
  2. 结构函数回来的联合类型。
interface Padder {
  getPaddingString(): string
}
class SpaceRepeatingPadder implements Padder {
  constructor(private numSpaces: number) { }
  getPaddingString() {
    return Array(this.numSpaces + 1).join(" ");
  }
}
class StringPadder implements Padder {
  constructor(private value: string) { }
  getPaddingString() {
    return this.value;
  }
}
function getRandomPadder() {
  return Math.random() > 0.5 ? new SpaceRepeatingPadder(4) : new StringPadder("  ");
}
// 类型为SpaceRepeatingPadder | StringPadder
let padder: Padder = getRandomPadder();
if (padder instanceof SpaceRepeatingPadder) {
  padder; // 类型缩小为'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
  padder; // 类型缩小为'StringPadder'
}

空值类型护卫

运用类型维护来去除联合类型中的 null 与 JavaScript 写法共同。

function f(sn: string | null): string {
  if (sn == null) {
    return "default";
  }
  else {
    return sn;
  }
}

也能够运用短路运算符:

function f2(sn: string | null): string {
  return sn ?? "default";
}

在编译器无法消除 nullundefined 的情况下,能够运用类型断言运算符手动删除它们,语法是后缀添加 !,这将从标识符的类型中删除 nullundefined 类型。

interface UserAccount {
  id: number;
  email?: string;
}
function getUser(id: string): UserAccount | undefined {
  return { email: '' } as any;
}
const user = getUser("admin");
user.id; // Object is possibly 'undefined'.
if (user) {
  user.email.length; // Object is possibly 'undefined'.
}
// 假如确认这些目标或字段存在,则添加短路可空性
user!.email!.length;