typescript基础篇(7):类型检查机制

2020-08-28 15:33:51 浏览数 (1)

7. 类型检查机制

所谓类型检查机制,就是编程语言编译器在做类型检查时,所秉持的原则,以及表现出的行为。

7.1 类型推断

TS作为一门灵活的强类型语言:如果你声明一个变量,不一定都要做类型注解,ts会根据某些规则,自动推断出变量的类型。

先来看基础类型的推断:

代码语言:javascript复制
let name1:string;
let name2 = ''; // 推断为string
let name // 推断为any

let num = 1 // 推断为number

let arr = [] // 推断为any[]
let arr2 = [1] //推断为 number[]
// 当需要从几个表达式中推断类型时候,会使用这些表达式的类型来推断出一个最合适的通用类型。
let arr3 = [1,null] // 推断为 number|null

在函数中也是如此:

代码语言:javascript复制
// 自动推断入参和返回值都是number
const c = (x = 1) => x   1

以上都是等号右边向等号左边的推断,也存在等号左边向右边的推断(上下文推断):

代码语言:javascript复制
window.onmousedown = function(mouseEvent) {
  console.log(mouseEvent.button) //<- 报错
}

window.onmousedown = function(mouseEvent: any) {
  console.log(mouseEvent.button) //<- 可以直接使用mousedown的专属属性。
}

有时候,你对你的代码有充足的自信,且想要推翻对ts的推论。这时候需要用到类型断言——回想鸭子模型,如果一个动物不符合鸭子的特征,那么开发者可以“断言”,让它被归类为鸭子。此处再复习下:

代码语言:javascript复制
// 试改造如下代码
let foo = {}
foo.bar = 1 // <-报错

思路是定义一个接口,让它有bar这个属性:

代码语言:javascript复制
interface Foo {
  bar: number
}
let foo = {} as Foo // <- 不报错
// foo.bar = 1

到这里为止,代码不报错,相当于使用类型断言绕过了代码推断。但这个写法并不好。如果我不写foo.bar=1,这里的检查就漏过去了。建议是直接注解foo:

代码语言:javascript复制
let foo: Foo = {
  bar: 1,
}

所以,不要滥用断言

7.2 类型兼容性

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

代码语言:javascript复制
X兼容Y:X(目标类型)= Y(源类型)

举个例子,当tsconfig.json"strictNullChecks": false时,以下操作时被允许的:

代码语言:javascript复制
let s: string = "a"
s = null

为什么呢?因为在typescript中,null被默认为字符串的子类型。因此可以说:字符串类型兼容null类型。

7.2.1 对象的兼容性

再来看一个问题:

代码语言:javascript复制
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 // <-报错:类型 "X" 中缺少属性 "c",但类型 "Y" 中需要该属性。

让我们重新温故下鸭子模型:

" 当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。 "——在鸭子类型中,关注的不是对象的类型本身,而是它是如何使用的。

只要Y接口具备X接口的所有必要成员,那么X就兼容Y(成员少的兼容成员多的)。

7.2.2 函数的兼容性

对于函数的兼容,包含以下条件:

1.参数的个数

代码语言:javascript复制
// 声明一个Handler类型
type Handler = (a: number, b: number) => void
// 高阶函数
const hoc = (handler: Handler) => handler

// 1.参数个数<=Handler参数
let handler1 = (a: number) => {}
hoc(handler1)

// 参数个数>Handler参数
let handler2 = (a: number, b: number, c: number) => {}
// hoc(handler2) //<-报错
代码语言:javascript复制
// 可选参数和剩余参数
let a = (p1: number, p2: number) => {}
let b = (p1?: number, p2?: number) => {}
let c = (...args: number[]) => {}

// 固定参数:可以兼容可选参数和不定参数
a = b
a = c

// 可选参数
// b = a // <- 报错,可配置tsconfig去除
// b = c // <- 报错,可配置tsconfig去除

// 不定参数
c = a
c = b

如果通过接口来定义两个函数的入参:

代码语言:javascript复制
interface Point3D {
  x: number
  y: number
  z: number
}

interface Point2D {
  x: number
  y: number
}

let p3d = (point: Point3D) => {}
let p2d = (point: Point2D) => {}

p3d = p2d
p2d = p3d // <-报错

在此,对于定义了接口类型的函数参数个数多的兼容参数个数少的

1.参数类型匹配

如果两个函数参数类型无法对应,二者无法兼容

代码语言:javascript复制
// 声明一个Handler类型
type Handler = (a: number, b: number) => void
// 高阶函数
const hoc = (handler: Handler) => handler

let handler3 = (a: string) => {}
hoc(handler3) // <-报错:handler3 和 Handler不兼容

1.返回值类型:

目标函数的返回值类型必须与源函数的返回值相同,或为其子类型。

代码语言:javascript复制
let f = () => ({ name: "djtao" })
let g = () => ({ name: "djtao", job: "coder" })

f = g
g = f //<-报错

1.函数重载

具体实现的函数中,参数不得多于重载签名,参数类型,返回类型只能是重载签名的类型。

代码语言:javascript复制
// 重载列表
function overload(a: number, b: number): number
function overload(a: string, b: string): string

function overload(a: any, b: any): any {}
7.2.3 枚举类型的兼容性

枚举和number可以相互兼容。

代码语言:javascript复制
enum Fruit {
  Apple,
  Banana,
}

let fruit: Fruit.Apple = 1
let n_fruit: number = Fruit.Apple

但是枚举类型之间完全不兼容

代码语言:javascript复制
let color: Color.Red = Fruit.Apple // <-报错
7.2.4 类的兼容性

现有两个类A和B:

代码语言:javascript复制
class A {
  id: number = 1
  constructor(p: number, q: number) {}
}

class B {
  static s = 1
  id: number = 2
  constructor(p: number) {}
}

let aa = new A(1, 2)
let bb = new B(2)

aa = bb
bb = aa

比较两个类是否兼容:静态成员和构造函数是不参与比较的。在此基础上,如果拥有相同的实例成员(在上面例子中,相同实例成员为id),那么二者可以相互兼容。(此情形包括父类和子类之间)

两个类,公共成员一致,如果有私有成员(private):只需要考虑三种情况:

•一个有,一个没有:没有的兼容有的•子类有特殊的私有属性:父类兼容子类。•其它情况:相互不兼容

7.2.5 泛型兼容性

对于泛型接口,如果不定义任何成员,哪怕具体传参不同,都是相互兼容:

代码语言:javascript复制
interface Empty<T> {}

let obj1: Empty<number> = {}
let obj2: Empty<string> = {}

obj1 = obj2
obj2 = obj1

但如果我在Empty中定义了一个成员:

代码语言:javascript复制
interface Empty<T> {
  value:<T>
}

二者互不兼容。也就是说:当成员类型被定义了,泛型接口之间就不能兼容

对于泛型函数:如果两个泛型函数的定义相同,没有指定参数类型。那么两个函数之间是完全兼容的。

代码语言:javascript复制
let log1 = <T>(x: T): T => {
  console.log("x")
  return x
}

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

log1 = log2
log2 = log1
7.2.6 小结

类型兼容性的用例非常多且繁杂,但是都是基于鸭子模型。为此,总结的规律是:

•结构之间:成员少的兼容成员多的•函数之间:参数多的兼容成员少的

7.3 类型保护

先看个例子:

我们用枚举类型实现一个语言选择方法,逻辑是判断是否强类型,是则执行helloJava并返回Java,否则执行helloJavascript并返回JavaScript。目前已完成的代码如下:

代码语言:javascript复制
enum Type {
  Strong,
  Weak,
}

class Java {
  helloJava() {
    console.log("hello java")
  }
}

class JavaScript {
  helloJavaScript() {
    console.log("hello JavaScript")
  }
}

const getLanguage = (type: Type) => {
  let lang = type === Type.Strong ? new Java() : new JavaScript()
  return lang
}

getLanguage(Type.Strong)

接下来完成核心的getLanguage:

代码语言:javascript复制
const getLanguage = (type: Type) => {
  let lang = type === Type.Strong ? new Java() : new JavaScript()
    // 报错:类型“Java | JavaScript”上不存在属性...
  if (lang.helloJava) {
    lang.helloJava()
  } else {
    lang.helloJavaScript()
  }
  return lang
}

从报错信息看,ts是把lang作为了一种联合类型,以至于访问helloJava出错。所以我们使用类型断言:

代码语言:javascript复制
const getLanguage = (type: Type) => {
  let lang = type === Type.Strong ? new Java() : new JavaScript()
  // 注意,不能写`(lang as Java).helloJava`,3.7之后会校验
  if (!!(lang as Java).helloJava) {
    ;(lang as Java).helloJava()
  } else {
    ;(lang as JavaScript).helloJavaScript()
  }

  return lang
}

getLanguage(Type.Strong)

这段lang as的断言写的有些非主流了,为了正常调用,不得不多次进行断言。而类型保护机制就是为了解决这类问题而诞生的。ts能够在特定的区块中保证变量属于某种确定的类型,你可以在此区块中放心使用此类型的使用和方法。

以下阐述四种创建此区块的方法。

7.3.1 instanceof

Instanceof可以判断一个对象是否属于某种类型的实例。

代码语言:javascript复制
const getLanguage = (type: Type) => {
  let lang = type === Type.Strong ? new Java() : new JavaScript()
  // 1.instanceof
  if (lang instanceof Java) {
    lang.helloJava()
  } else {
    lang.helloJavaScript()
  }
  return lang
}
7.3.2 in关键字

逻辑和instanceof类似,判断某个属性/方法是否存在。

代码语言:javascript复制
const getLanguage = (type: Type) => {
  let lang = type === Type.Strong ? new Java() : new JavaScript()
  // 2.in关键字
  if ("helloJava" in lang) {
    lang.helloJava()
  } else {
    lang.helloJavaScript()
  }
  return lang
}
7.3.3 typeof
代码语言:javascript复制
    // 3. typeof
  if (typeof x == "string") {
    // x可以直接获得字符串方法
  } 
7.3.4 类型保护函数
代码语言:javascript复制
// 4.类型保护函数
function isJava(lang: Java | JavaScript): lang is Java {
  return (lang as Java).helloJava !== undefined
}

const getLanguage = (type: Type) => {
  let lang = type === Type.Strong ? new Java() : new JavaScript()
  if (isJava(lang)) {
    lang.helloJava()
  } else {
    lang.helloJavaScript()
  }
  return lang
}

0 人点赞