likes
comments
collection
share

🌐 TS-类型检查机

作者站长头像
站长
· 阅读数 3

TS-类型检查机制 类型推断/类型兼容

我们来介绍TS的类型检查机制。类型检查机制是指 'TS编译器在做类型检查时,所秉承的一些原则,以及表现出的一些行为。' 作用:辅助开发,提高开发效率。

我们会有三个章节来讲解这块内容:

  • 类型推断
  • 类型兼容性
  • 类型保护

本章节我们讲解第一部分,类型推断。所谓的类型推断就是指 '不需要指定变量的类型(函数返回值类型)TS就可以根据某些规则自动的推断出一个类型。'

类型推断又分为三部分:

  • 基础类型推断
  • 通用类型推断
  • 上下文类型推断

基础类型推断

基础类型推断是 TS 中比较常见的类型推断。

比如我们在初始化一个变量的时候 如果不指定变量类型 ,TS就会自动的推断为 any 类型。

let count;

我们可以在 TypeScript Playground 中查看:

TypeScript 作为 JavaScript 的超集,在开发过程中不可避免要引用其他第三方的 JavaScript 的库。虽然通过直接引用可以调用库的类和方法,但是却无法使用TypeScript 诸如类型检查等特性功能。.D.TS 就是为了解决这个问题孕育而生。

.D.TS 是一个声明文件,这个文件是里面不包含具体的源代码(函数/方法体/对象外形)保留导出类型声明。

declare let count: any;

如果我们把一个数组赋值给这个 count 变量:

let count = [];

那在 TS 中会自动推断出 数组成员默认是 any 类型。

declare let count: any[];

如果我们给这个数组添加一个成员 1 :

let count = [1];

那么就会自动的推断为是 Number 类型的数组。

declare let count: number[];

在比如当我们给函数参数设置默认值也会发生类型推断

let func = (x=1)=>{}

这个时候参数 x 被推断为 number 类型。当我们设置返回值的时候也会发生类型推断

let func = (x= 1)=>x+1

默认 func 返回值是number 类型。

declare let func: (x?: number) => number;

通用类型推断

当需要从多个类型中推断一个类型,TS就会推断出一个尽可能兼容所有类型的通用类型。

let count = [1, null];

比如我们给 count 数组中添加不同类型的成员, null 和 number 类型的值是不兼容的,所有 count 就会被推断成 null 和 number 的联合类型。

declare let count: (number | null)[];

以上的类型推断都是从右至左的推断,根据表达式右侧的值来推断表达式左侧变量的类型。 还有一种类型推断它是相反的 它是从左到右,这个就是上下文类型推断它通常会发生在事件处理中。

window.onkeydown = function(e){
  console.log(e)
}

这个时候就会发生上下文类型推断,TS 会根据左侧的事件类型来推测出右侧事件对象的类型KeyboardEvent。

查看示例代码:www.typescriptlang.org/play

类型兼容性

概述:当一个类型Y可以被赋值给另外一个类型X时,我们就可以说类型X兼容类型Y。

X兼容Y:X(目标类型) = Y(源类型)

示例:

let s:string = "hello ts";

定义了一个字符串变量 s, 在tsconfig 中关闭了 stricNullCheCks:false ,字符串变量是可以被赋值为null 的。

s = null;

这个时候我们可以理解 字符型是可以兼容 null 类型的。 也就是null 是字符型的子类型。之所以我们要讨论类型兼容性的问题是因为 TS 允许我们把不同类型的变量,相互赋值。 虽然在某种程度上会产生不可靠的行为,但确增加了语言的灵活性。

类型兼容性的例子会广泛出现在 接口 、函数、类中。 那首先我们来看下两个接口是怎么样兼容的。

interface X{
  a: any;
  b: any;
}

interface Y{
  a: any;
  b: any;
  c: any;
}

let x:X = {a:1, b:2};
let y:Y = {a:1, b:2, c:3};

x = y;
y = x;

此时后者会报错:

Declaration or statement expected.

这里我们可以看到,只要Y接口具备 X接口所有的属性,那么Y接口的变量就能赋值给X接口的变量。 反之则会报错,这里在次体现了TS 类型检查原则,也就是鸭式变型法。 我们在回顾下当一只鸟走起来像鸭子叫起来像鸭子这只鸟就可以称之为是一只鸭子。

总结一句:"源类型必须具备目标类型的所有属性就可以进行赋值"。 大白话讲就是接口成员相互兼容的时候成员少的会兼容成员多的。

函数兼容性

函数兼容通常会发生在两个函数赋值的情况下。

示例代码:

type Handler = (a:number,b:number)=>void;
function ref(handler: Handler){
 return handler;
}

let handler1 = (a:number,b:number)=>{}
ref(handler1)

如上函数作为参数的情况下,在调用ref 高阶函数的时候会接收一个类型为Handler的参数,在把handler作为参数返回出去。

此时Handler类型就是目标类型,传入的参数就是源类型。 如果要目标函数兼容源函数它们需要同时满足三个条件。

1:参数的个数

要求源函数的参数个数一定要少于或等于目标函数的个数。

let handler2 = (a:number,b:number,c:number)=>{}
ref(handler2)

如果大于:

Argument of type '(a: number, b: number, c: number) => void' is not assignable to parameter of type 'Handler'.

编译器就会报出类型兼容的错误信息。

特殊情况

当函数中拥有可选参数的时候会遵循另外一套原则。例如我们这边有三个函数一个是拥有固定参数/可选参数/剩余参数。

let fn = (a:number, b:number)=>{}
let optional = (a?:number, b?:number)=>{}
let surplus= (...args:number[])=>{}

原则1:固定参数是可以兼容可选参数/剩余参数。

fn = optional;
fn = surplus;

原则2:可选参数不兼容固定参数/剩余参数的。

optional = fn;
optional = surplus;

报错: 类型'(a: number, b: number) => void' 不能分配给类型 '(a?: number , b?: number ) => void'.

Type '(a: number, b: number) => void' is not assignable to type '(a?: number | undefined, b?: number | undefined) => void'.

如果你非要做到兼容也可以我们需要去找到 tsconfig.json 文件中的strictFunctionTypes选项 设置为false就可以了。

strictFunctionTypes:false

strictFunctionTypes 是否启用对函数类型的严格检查。

strictNullChecks 是否启用严格的null检查。

原则3:剩余参数可以兼容可选参数/ 固定参数

surplus= fn;
surplus= optional;

以上就是TS对函数兼容中对参数的要求。

2:参数类型

参数类型必须要匹配

let fn = (a:number, b:number)=>{}
let fn1 = (a:string, b:string)=>{}
fn = fn1;

报错类型不兼容:

Type '(a: string, b: string) => void' is not assignable to type '(a: number, b: number) => void'.

上述示例是一个基础的类型,比较好判断如果是一个对象类型就比较复杂了。说他复杂的原因是跟我们之前所学习的东西有点冲突:

interface X{
  a: any;
  b: any;
}

interface Y{
  a: any;
  b: any;
  c: any;
}

let x:X = {a:1, b:2};
let y:Y = {a:1, b:2, c:3};

x = y;
y = x;

刚刚是后者会报错:成员少的会兼容成员多的 只要Y接口具备 X接口所有的属性,那么Y接口的变量就能赋值给X接口的变量。

但是在函数中参数对象的兼容刚好是相反的 成员多的会兼容成员少的。

interface X{
  a: any;
  b: any;
}

interface Y{
  a: any;
  b: any;
  c: any;
}

let fn2 = (x:X)=>{}
let fn3 = (y:Y)=>{}
fn2= fn3;
fn3 = fn2;

此时把fn3 字段多的赋值给 fn2 会报错。 这里大家在学习的时候非常容易混淆,教给大家一个办法就是在参数对象中你把 interface 接口对象中的成员拆分为单个的参数, 这样参数多的就会兼容参数少的,跟我们之前所学的就不违背了。

如果要做到fn2 兼容 fn3 也很容易我们只需要再次关闭刚刚的strictFunctionTypes配置就行了。 这种函数参数可以相互赋值的情况,我们叫做函数参数的双向协变。 这种情况下可以允许我们把一个精确的类型赋值给一个不那么精确的类型。这样做很方便省略了你把一个不精确的类型断言成精确类型的过程。

那接下来我们在看下函数类型兼容的第三个条件。

3:返回值类型

TS 要求我们目标函数的返回值类型必须与源函数的返回值类型一致,或者是它的子类型。

var f=()=>({name:"老李"});
var g=()=>({name:"老王", age:30});

f= g;
g = f;

这里f 就能兼容g, 反过来g 是不兼容f的。 因为f函数返回值是g函数返回值的子类型。 同样这里是成员少的兼容成员多的,与鸭式变型法是一样的。

4:函数重载

function extend(a:string, b:string):string;
function extend(a:number, b:number):number;
function extend(a:number, b:number):any{}

函数重载分为两个部分, 重载列表/在这个列表中定义了两个函数extend, 然后就是函数的具体实现。 列表中的函数就是目标函数,而函数的具体实现就是源函数。

程序在运行的时候,编译器会查找这个重载列表,然后使用第一个匹配的定义来执行下面的extend 函数。 所以在重载列表中目标函数的参数要多余或等于源函数的参数,而且返回值的类型也要符合相应的要求。

枚举类型的兼容性

枚举类型和number类型是可以完全兼容的。

enum m {one, two }
enum n {first }

let flag:n = 2;

在这我们定义了一个变量flag,它的类型是枚举类型,我们可以给他赋值任意的数字。反过来我们定义一个 f 的变量类型是number,也可以被赋值给一个枚举类型。

enum m {one, two }
enum n {first }

let f:number = n.first;

所以我们可以看到枚举跟number直接是可以相互兼容的。 但是枚举之间是相互不兼容的。

enum m {one, two }
enum n {first }

let z:m.one = n.first;

这里会报错: 类型 n 不能分配给 m.one 。

Type 'n' is not assignable to type 'm.one'.

类兼容性

类兼容性跟interface 比较相似他们只比较结构。 这里需要注意在比较类是否兼容的时候,静态成员跟构造函数是不参与比较的。 如果两个类是具有相同的实例成员那他们的实例就可以相互兼容。

class A{
  constructor(a:number, b:number){}
  index:number = 0;
}

class B{
  static c = 2;
  constructor(a:number, b:number){}
  index:number = 1;
}

let a = new A(1,2);
let b = new B(3,4);

这里我们定义了两个类 A / B, 分别创建了两个实例我们来看下这两个实例是否兼容。

a = b;
b = a;

运行代码可以看到这两个实例是完全兼容的,因为他们都具备一个实例属性index。 而构造函数和静态成员是不进行比较的。

但如果我们在B这个类中定义一个私有成员呢?

class A{
  constructor(a:number, b:number){}
  index:number = 0;
}

class B{
  static c = 2;
  constructor(a:number, b:number){}
  index:number = 1;
  private s:string= '';
}

let a = new A(1,2);
let b = new B(3,4);

a = b;
b = a;

这时 a 赋值给 b 就会报错。 类型 "A"中缺少属性"s",但类型"B"中需要属性"s"。

Property 's' is missing in type 'A' but required in type 'B'.

泛型兼容性

interface getDate<T>{

}

let data:getDate<Number> = {};
let flags:getDate<string> = {};
data = flags;

我们先来看一个泛型接口,这个接口没有任何的成员,然后我们定义了两个变量。 这两个变量都是这个接口类型只不过具体传递的参数类型不同。 此时这两个变量是完全兼容的,这是因为这个泛型接口没有任何的成员。接下来我们给它加一个成员来看下。

interface getDate<T>{
    value:T
}

var data:getDate<Number> = {};
var flags:getDate<string> = {};

data = flags;

这个时候类型就不兼容了,也就是说只有类型参数T 被接口成员使用的时候才会影响泛型的兼容性。

泛型函数

这里我们定义了两个完全相同的泛型函数,那么它们之间是不是兼容的呢?

let log1 = <T>(x:T):T => {
   console.log(x);
   return x;
}

let log2 = <U>(y:U):U => {
   console.log(y);
   return y;
}

log1 = log2

运行代码发现是完全兼容的,也就是说如果两个泛型函数的定义相同,但是没有指定类型参数那么它们之间也是可以相互兼容的。