# 工作原理相关概念

# 类型推断

# 常见推断

类型推断(type inference),TS 会自动的去尝试分析变量的类型。例如:

// 这就是典型的类型推断,它们的类型是 number 而且值永远都不会变的
const firstnumber = 1
const secondNumber = 2
const total = firstNumber + secondNumber

# 类型联合

当我们定义一个数组或元组这种包含多个元素的值的时候,多个元素可以有不同的类型,这种时候 TypeScript 会将多个类型合并起来,组成一个联合类型:

let arr = [1, "a"];  // 此时的 arr 的元素被推断为string | number
arr = ["b", 2, false];  // error 不能将类型“false”分配给类型“string | number”

再来看一个案例:

let value = Math.random() * 10 > 5 ? 'abc' : 123
value = false // error 不能将类型“false”分配给类型“string | number”

这里我们给value赋值为一个三元操作符表达式,Math.random() * 10的值为0-10的随机数。

这里判断,如果这个随机值大于5,则赋给value的值为字符串’abc’,否则为数值123。

所以最后编译器推断出的类型为联合类型string | number,当给它再赋值为false的时候就会报错。

# 下文类型

我们上面讲的两个例子都是根据=符号右边值的类型,推断左侧值的类型。现在要讲的上下文类型则相反,它是根据左侧的类型推断右侧的一些类型,先来看例子:

window.onmousedown = function(mouseEvent) {
  console.log(mouseEvent.a); // error 类型“MouseEvent”上不存在属性“a”
};

我们可以看到,表达式左侧是 window.onmousedown(鼠标按下时发生事件),TypeScript 会推断赋值表达式右侧函数的参数是事件对象。当访问不存在属性的时候,就会报错。

# 类型注解

类型注解(type annotation) ,告诉 TS 变量是什么类型。例如:

// 当 TS 无法推断出变量类型的时候需要添加类型注解
function getTotal(firstNumber: number, secondNumber: number) {
  return firstNumber + secondNumber
}
const total = getTotal(1, 2)

// 其他的情况
interface Person {
  name: string
}
const rawData = '{"name": "zws"}'
const newData: Person = JSON.parse(rawData)

// 一个变量是一个数字类型,后续要变成字符串。类似或运算符
let temp: number | string = 123
temp = '456'

# 静态类型

/** count
 * 不可以为其他类型
 */
const count = 2021 // 这里根据类型推断,可以省略 :number

编辑器会提示数字类型的所有方法如:

image-20210720142046149

# 类型断言

# 什么是类型断言?

TypeScript 允许你覆盖它的推断,并且能以你任何你想要的方式分析它,这种机制被称为「类型断言」。

它之所以不被称为「类型转换」,是因为转换通常意味着某种运行时的支持。但是,类型断言纯粹是一个编译时语法,同时,它也是一种为编译器提供关于如何分析代码的方法。

类型断言的一个常见用例是当你从 JavaScript 迁移到 TypeScript 时:

const foo = {};
foo.bar = 123; // Error: 'bar' 属性不存在于 ‘{}’
foo.bas = 'hello'; // Error: 'bas' 属性不存在于 '{}'

这里的代码发出了错误警告,因为 foo 的类型推断为 {},即没有属性的对象。

# 解决办法

  • as运算符
  • <type>value写法

如上例:不能在它的属性上添加 barbas,可以通过类型断言来避免此问题:

interface Foo {
  bar: number;
  bas: string;
}

const foo = {} as Foo;

// 或者写成
// const foo = <Foo>{}
foo.bar = 123;
foo.bas = 'hello';

因此,为了一致性,我们建议你使用 as foo 的语法来为类型断言。

再举一个例子:

const getLength = (target: string | number): number => {
  if (target.length) { // 类型"string | number"上不存在属性"length"
    return target.length; // 类型"number"上不存在属性"length"
  } else {
    return target.toString().length;
  }
};

按照提到过的两种解决思路:

const getStrLength = (target: string | number): number => {
  if ((<string>target).length) { // 这种形式在JSX代码中不可以使用,而且也是TSLint不建议的写法
    return (target as string).length; // 这种形式是没有任何问题的写法,所以建议大家始终使用这种形式
  } else {
    return target.toString().length;
  }
};

# 最佳实践as vs <type>

最初的断言语法如下所示:

let foo: any;
let bar = <string>foo; // 现在 bar 的类型是 'string'

然而,当你在 JSX 中使用 <foo> 的断言语法时,这会与 JSX 的语法存在歧义:

let foo = <string>bar;

因此,为了一致性,我们建议你使用 as 的语法来为类型断言。

类型断言被认为是有害的,为什么呢?

在很多情景下,断言能让你更容易的从遗留项目中迁移(甚至将其他代码粘贴复制到你的项目中),然而,你应该小心谨慎的使用断言。

interface Foo {
  bar: number;
  bas: string;
}

const foo = {} as Foo;

或者写成:

interface Foo {
  bar: number;
  bas: string;
}

const foo = <Foo>{
  // 编译器将会提供关于 Foo 属性的代码提示
  // 但是开发人员也很容易忘记添加所有的属性
  // 同样,如果 Foo 被重构,这段代码也可能被破坏(例如,一个新的属性被添加)。
};

这也会存在一个同样的问题,如果你忘记了某个属性,编译器同样也不会发出错误警告。使用一种更好的方式:

interface Foo {
  bar: number;
  bas: string;
}

const foo: Foo = {
  // 编译器将会提供 Foo 属性的代码提示
};

在某些情景下,你可能需要创建一个临时的变量,但至少,你不会使用一个承诺(可能是假的),而是依靠类型推断来检查你的代码。

# 类型保护

类型保护即是:指的是TypeScript 能够在特定的区块(类型保护区块)中保证变量属于某种特定的类型。 可以在此区块中放心地引用此类型的属性,或者调用此类型的方法。

来看看如下的示例:

const valueList = [123, "abc"];

const getRandomValue = () => {
  const number = Math.random() * 10; // 这里取一个[0, 10)范围内的随机值
  if (number < 5) return valueList[0]; // 如果随机数小于5则返回valueList里的第一个值,也就是123
  else return valueList[1]; // 否则返回"abc"
};

const item = getRandomValue();

if (item.length) {
  // error 类型“number”上不存在属性“length”
  console.log(item.length); // error 类型“number”上不存在属性“length”
} else {
  console.log(item.toFixed()); // error 类型“string”上不存在属性“toFixed”
}

上面的逻辑在JS中可以,但是在TS中,无法推测item的类型,所以报错。

这里可以使用类型断言:

if ((<string>item).length) {
  console.log((<string>item).length);
} else {
  console.log((<number>item).toFixed());
}

类型保护的三种方法:

  • 自定义类型保护;
  • typeof 类型保护;
  • Instanceof 类型保护;

# 自定义类型保护

通过定义一个返回值类型是"参数名 is type"的语句,来指定传入这个类型保护函数的某个参数是什么类型。

如下面的isString方法:

const valueList = [123, "abc"];

// 这里取一个[0, 10)范围内的随机值
// 如果随机数小于5则返回valueList里的第一个值,也就是123
// 否则返回"abc"
const getRandomValue = () => {
  const number = Math.random() * 10; 
  if (number < 5) return valueList[0]; 
  else return valueList[1]; 
};

function isString(value: number | string): value is string {
  const number = Math.random() * 10
  return number < 5;
}

const item = getRandomValue();

if (isString(item)) {
  console.log(item.length); // 此时item是string类型
} else {
  console.log(item.toFixed()); // 此时item是number类型
}

# typeof类型保护

只需要在 if 的判断逻辑地方使用 typeof 关键字即可判断一个值的类型。

if (typeof item === "string") {
  console.log(item.length);
} else {
  console.log(item.toFixed());
}

这样直接写也是可以的,效果和自定义类型保护一样。但是在 TS 中,对 typeof 的处理还有些特殊要求:

  • 只能使用=!两种形式来比较,比如使用(typeof item).includes('string')也能做判断,但是不准确;
  • type 只能是numberstringbooleansymbol四种类型

typeof的缺点:typeof xxx的结果还有objectfunctionundefined,像数组与对象就不能很好的区分。例子:

const valueList = [{}, () => {}];

const getRandomValue = () => {
  const number = Math.random() * 10;
  if (number < 5) {
    return valueList[0];
  } else {
    return valueList[1];
  }
};

const res = getRandomValue();
if (typeof res === "object") {
  console.log(res.toString());
} else {
  // error 无法调用类型缺少调用签名的表达式。类型“{}”没有兼容的调用签名
  console.log(ress()); 
}

# instanceof类型保护

instanceof操作符是 JS 中的原生操作符,它用来判断一个实例是不是某个构造函数创建的,或者是不是使用 ES6 语法的某个类创建的。

在 TS 中,使用 instanceof 操作符同样会具有类型保护效果,来看例子:

class CreateByClass1 {
  public age = 18;
  constructor() {}
}
class CreateByClass2 {
  public name = "toimc";
  constructor() {}
}

function getRandomItem() {
  // 如果随机数小于0.5就返回CreateByClass1的实例,否则返回CreateByClass2的实例
  return Math.random() < 0.5 ? new CreateByClass1() : new CreateByClass2(); 
}

const item = getRandomItem();

// 这里判断item是否是CreateByClass1的实例
if (item instanceof CreateByClass1) { 
  console.log(item.age);
} else {
  console.log(item.name);
}

# *类型兼容

  • 函数参数个数: 如果对函数 y 进行赋值,那么要求 x 中的每个参数都应在 y 中有对应,也就是 x 的参数个数小于等于 y 的参数个数;

    let x = (a: number) => 0;
    let y = (b: number, c: string) => 0;
    
    
    y = x; // 没问题
    
    x = y; // error Type '(b: number, s: string) => number' is not assignable to type '(a: number) => number'
    
  • 函数参数类型: 这一点其实和基本的赋值兼容性没差别,只不过比较的不是变量之间而是参数之间;

    let x = (a: number) => 0;
    let y = (b: string) => 0;
    let z = (c: string) => false;
    x = y; // error 不能将类型“(b: string) => number”分配给类型“(a: number) => number”。
    x = z; // error 不能将类型“(c: string) => boolean”分配给类型“(a: number) => number”。
    
  • 剩余参数和可选参数: 当要被赋值的函数参数中包含剩余参数(…args)时,赋值的函数可以用任意个数参数代替,但是类型需要对应,可选参数效果相似;

    const getNum = (
      arr: number[],
      callback: (arg1: number, arg2?: number) => number // 这里指定第二个参数callback是一个函数,函数的第二个参数为可选参数
    ): number => {
      return callback(...arr); // error 应有 1-2 个参数,但获得的数量大于等于 0
    };
    
  • 函数参数双向协变: 即参数类型无需绝对相同;

    let funcA = function(arg: number | string): void {};
    let funcB = function(arg: number): void {};
    // funcA = funcB 和 funcB = funcA都可以
    

    在这个例子中,funcA 和 funcB 的参数类型并不完全一样,funcA 的参数类型为一个联合类型 number | string,而 funcB 的参数类型为 number | string 中的 number,他们两个函数也是兼容的。

  • 函数返回值类型: 这一点和函数参数类型的兼容性差不多,都是基础的类型比较;

    let x = (a: number): string | number => 0;
    let y = (b: number) => "a";
    let z = (c: number) => false;
    x = y;
    x = z; // 不能将类型“(c: number) => boolean”分配给类型“(a: number) => string | number”
    
  • 函数重载: 要求被赋值的函数每个重载都能在用来赋值的函数上找到对应的签名;

    带有重载的函数,要求被赋值的函数的每个重载都能在用来赋值的函数上找到对应的签名,来看例子:

    function merge(arg1: number, arg2: number): number; // 这是merge函数重载的一部分
    function merge(arg1: string, arg2: string): string; // 这也是merge函数重载的一部分
    function merge(arg1: any, arg2: any) { // 这是merge函数实体
      return arg1 + arg2;
    }
    function sum(arg1: number, arg2: number): number; // 这是sum函数重载的一部分
    function sum(arg1: any, arg2: any): any { // 这是sum函数实体
      return arg1 + arg2;
    }
    let func = merge;
    func = sum; // error 不能将类型“(arg1: number, arg2: number) => number”分配给类型“{ (arg1: number, arg2: number): number; (arg1: string, arg2: string): string; }”
    

    上面例子中,sum函数的重载缺少参数都为string返回值为string的情况,与merge函数不兼容,所以赋值时会报错。

  • 枚举:数字枚举成员类型与数值类型兼容,字符串枚举成员与字符串类型不兼容;

    enum Status {
      On,
      Off
    }
    enum Color {
      White,
      Black
    }
    let s = Status.On;
    s = Color.White; // error Type 'Color.White' is not assignable to type 'Status'
    
    s = 'toimc' // error 不能将类型“"toimc"”分配给类型“Status”
    
  • 类:比较的主要依据是实例成员,但是私有成员和受保护成员也会影响兼容性;

    比较两个类类型的值的兼容性时,只比较实例的成员,类的静态成员和构造函数不进行比较:

    class Animal {
      static age: number;
      constructor(public name: string) {}
    }
    class People {
      static age: string;
      constructor(public name: string) {}
    }
    class Food {
      constructor(public name: number) {}
    }
    let a: Animal;
    let p: People;
    let f: Food;
    a = p; // right
    a = f; // error Type 'Food' is not assignable to type 'Animal'
    

    类类型比较兼容性时,只比较实例的成员。

    这两个变量虽然类型是不同的类类型,但是它们都有相同字段和类型的实例属性name,而类的静态成员是不影响兼容性的,所以它俩兼容。而类Food定义了一个实例属性name,类型为number,所以类型为Food的f与类型为Animal的a类型不兼容,不能赋值。

    类的私有成员和受保护成员会影响兼容性。

    当检查类的实例兼容性时,如果目标(也就是要被赋值的那个值)类型(这里实例类型就是创建它的类)包含一个私有成员,那么源(也就是用来赋值的值)类型必须包含来自同一个类的这个私有成员,这就允许子类赋值给父类。

    来看例子:

    class Parent {
      private age: number;
      constructor() {}
    }
    class Children extends Parent {
      constructor() {
        super();
      }
    }
    class Other {
      private age: number;
      constructor() {}
    }
    
    const children: Parent = new Children();
    const other: Parent = new Other(); // 不能将类型“Other”分配给类型“Parent”。类型具有私有属性“age”的单独声明
    

    当指定 other 为 Parent 类类型,给 other 赋值 Other 创建的实例的时候,会报错。因为 Parent 的 age 属性是私有成员,外界是无法访问到的,所以会类型不兼容。

    使用 protected 受保护修饰符修饰的属性,也是一样的。

    class Parent {
      protected age: number;
      constructor() {}
    }
    class Children extends Parent {
      constructor() {
        super();
      }
    }
    class Other {
      protected age: number;
      constructor() {}
    }
    const children: Parent = new Children();
    const other: Parent = new Other(); // 不能将类型“Other”分配给类型“Parent”。属性“age”受保护,但类型“Other”并不是从“Parent”派生的类