TypeScript | 类型检查机制

3,041 阅读7分钟

TypeScript 的类型检查机制都是为了让开发者在编译阶段就可以直观的发现代码书写问题,养成良好的代码规范从而避免很多低级错误。

TypeScript 类型检查机制包括 类型推断类型保护类型兼容性

类型推断

类型推断,不需要指定变量类型或函数的返回值类型,TypeScript 可以根据一些简单的规则推断其的类型。

类型推断主要用于那些没有明确指出类型的地方帮助确定和提供类型。类型推断是有方向的,要注意区分从左向右和从右向左两种推断的不同应用

基础类型推断

基础的类型推断发生在 初始化变量,设置默认参数和决定返回值时

// 初始化变量
let x = 3             // let x: number
let y = 'hello world' // let y: string
let z                 // let z: any

//设置默认参数和决定返回值时
function add(a:number, b:10) { // 参数 b 有默认值 10,被推断为 number 类型 ,返回值推断为 number
  return a + b
}
const obj = {
  a: 10,
  b: 'hello world'
}
obj.b = 15 // Error,Type '15' is not assignable to type 'string'

最佳通用类型推断

当需要从多个元素类型推断出一个类型时,TypeScript 会尽可能推断出一个兼容所有类型的通用类型。

比如声明一个数组:

let x = [1, 'imooc', null]
//数组被推断为 let x: (string | number | null)[] 联合类型

是否兼容 null 类型可以通过 tsconfig.json 文件中属性 strictNullChecks 的值设置为 true 或 false 来决定

上下文类型推断

前面两种都是根据从右向左流动进行类型推断,上下文类型推断则是从左向右的类型推断

例如定义一个 Animal 的类作为接口使用:

class Animal {
  public species: string | undefined
  public weight: number | undefined
}

const simba: Animal = {
  species: 'lion',
  speak: true  // Error, 'speak' does not exist in type 'Animal'
}

类型保护

类型保护是指缩小类型的范围,在一定的块级作用域内由编译器推导其类型,提示并规避不合法的操作,提高代码质量

我们可以通过 typeofinstanceofinis字面量类型 将代码分割成范围更小的代码块,在这一小块中,变量的类型是确定的

typeof

通过 typeof 运算符判断变量类型:

function reverse(target: string | number) {
  if (typeof target === 'string') {
    target.toFixed(2) // Error,在这个代码块中,target 是 string 类型,没有 toFixed 方法
    return target.split('').reverse().join('')
  }
  if (typeof target === 'number') { 
    //通过 typeof 关键字,将这个代码块中变量 target 的类型限定为 number 类型
    target.toFixed(2) // OK
    return +[...target.toString()].reverse().join('')
  }

  target.forEach(element => {}) // Error,在这个代码块中,target 是 string 或 number 类型,没有 forEach 方法
}

instanceof

instanceof 与 typeof 类似,区别在于 typeof 判断基础类型,instanceof 判断是否为某个对象的实例:

class User {
  public nickname: string | undefined
  public group: number | undefined
}

class Log {
  public count: number = 10
  public keyword: string | undefined
}

function typeGuard(arg: User | Log) {
  if (arg instanceof User) {
    arg.count = 15 // Error, arg 限定为User类型,无此属性
  }

  if (arg instanceof Log) {
    arg.count = 15 // OK,arg 限定为Log类型
  }
}

in

in 操作符用于确定属性是否存在于某个对象上,这也是一种缩小范围的类型保护。

class User {
  public nickname: string | undefined
  public groups!: number[]
}

class Log {
  public count: number = 10
  public keyword: string | undefined
}

function typeGuard(arg: User | Log) {
  if ('nickname' in arg) {
    // (parameter) arg: User,编辑器将推断在当前块作用域 arg 为 User 类型
    arg.nickname = 'imooc'
  }

  if ('count' in arg) {
    // (parameter) arg: Log,编辑器将推断在当前块作用域 arg 为 Log 类型
    arg.count = 15
  }
}

is 关键字

is 关键字一般用于函数返回值类型中,判断参数是否属于某一类型,并根据结果返回对应的布尔类型。

语法:prop is type

举例说明:

function isString(s: unknown): boolean {
  return typeof s === 'string'
}
function toUpperCase(x: unknown) {
  if(isString(x)) {
    //一个 unknown 类型的对象不能进行 toUpperCase()
    x.toUpperCase() // Error, Property 'toUpperCase' does not exist on type 'unknown'
  }
}

//用 is 关键字
const isString = (val: unknown): val is string => typeof val === 'string'
function toUpperCase(x: unknown) {
  if(isString(x)) {
    return x.toUpperCase()
  }
}
console.log(toUpperCase('aa')) //AA

拓展函数:

const isNumber = (val: unknown): val is number => typeof val === 'number'
const isString = (val: unknown): val is string => typeof val === 'string'
const isSymbol = (val: unknown): val is symbol => typeof val === 'symbol'
const isFunction = (val: unknown): val is Function => typeof val === 'function'
const isObject = (val: unknown): val is Record<any, any> => val !== null && typeof val === 'object'

function isPromise<T = any>(val: unknown): val is Promise<T> {
  return isObject(val) && isFunction(val.then) && isFunction(val.catch)
}

const objectToString = Object.prototype.toString
const toTypeString = (value: unknown): string => objectToString.call(value)
const isPlainObject = (val: unknown): val is object => toTypeString(val) === '[object Object]'

字面量类型保护

type Success = {
  success: true,
  code: number,
  object: object
}

type Fail = {
  success: false,
  code: number,
  errMsg: string,
  request: string
}

function test(arg: Success | Fail) {
  if (arg.success === true) {
    console.log(arg.object) // OK
    console.log(arg.errMsg) // Error, Property 'errMsg' does not exist on type 'Success'
  } else {
    console.log(arg.errMsg) // OK
    console.log(arg.object) // Error, Property 'object' does not exist on type 'Fail'
  }
}

类型兼容性

类型兼容性用于确定一个类型是否能赋值给其他类型

let address: string = 'Baker Street 221B'
let year: number = 2010
address = year // Error 类型 ‘number’ 不能赋值给类型 ‘string’

结构化

TypeScript 类型兼容性是基于结构类型的;结构类型只使用其成员来描述类型。

TypeScript 结构化类型系统的基本规则是,如果 x 要兼容 y,那么 y 至少具有与 x 相同的属性。比如:

interface User {
  name: string,
  year: number
}

let protagonist = {
  name: 'Sherlock·Holmes',
  year: 1854,
  address: 'Baker Street 221B'
}

let user: User = protagonist // OK

接口 User 中的每一个属性在 protagonist 对象中都能找到对应的属性,且类型匹配。另外,可以看到 protagonist 具有一个额外的属性 address,但是赋值同样会成功

比较两个函数

判断两个函数是否兼容,首先要看参数是否兼容,第二个还要看返回值是否兼容。

函数参数

let fn1 = (a: number, b: string) => {}
let fn2 = (c: number, d: string, e: boolean) => {}

fn2 = fn1 // OK
fn1 = fn2 // Error

将 fn1 赋值给 fn2 成立是因为:

  1. fn1 的每个参数均能在 fn2 中找到对应类型的参数
  2. 参数顺序保持一致,参数类型对应
  3. 参数名称不需要相同

将 fn2 赋值给 fn1 不成立,是因为 fn2 中的必须参数必须在 fn1 中找到对应的参数,显然第三个布尔类型的参数在 fn1 中未找到

参数类型对应即可,不需要完全相同

let fn1 = (a: number | string, b: string) => {}
let fn2 = (c: number, d: string, e: boolean) => {}

fn2 = fn1 // OK

函数返回值

let x = () => ({name: 'Alice'})
let y = () => ({name: 'Alice', location: 'Seattle'})

x = y // OK
y = x // Error 函数 x() 缺少 location 属性,所以报错

类型系统强制源函数的返回值类型必须是目标函数返回值类型的子类型。

如果目标函数的返回值类型是 void,那么源函数返回值可以是任意类型:

let x : () => void
let y = () => 'imooc'

x = y // OK

枚举的类型兼容性

//枚举与数字类型相互兼容:
enum Status {
  Pending,
  Resolved,
  Rejected
}

let current = Status.Pending
let num = 0

current = num
num = current

//不同枚举类型之间是不兼容的:
enum Status { Pending, Resolved, Rejected }
enum Color { Red, Blue, Green }

let current = Status.Pending
current = Color.Red // Error

类的类型兼容性

比较两个类类型数据时,只有实例成员会被比较,静态成员和构造函数不会比较。

class Animal {
  feet!: number
  constructor(name: string, numFeet: number) { }
}

class Size {
  feet!: number
  constructor(numFeet: number) { }
}

let a: Animal
let s: Size

a = s!  // OK
s = a  // OK

类的私有成员和受保护成员会影响兼容性。 允许子类赋值给父类,但是不能赋值给其它有同样类型的类。

class Animal {
  protected feet!: number
  constructor(name: string, numFeet: number) { }
}

class Dog extends Animal {}

let a: Animal
let d: Dog

a = d! // OK 子类可以赋值给父类。
d = a // OK 父类之所以能够给赋值给子类,是因为子类中没有成员

class Size {
  feet!: number
  constructor(numFeet: number) { }
}

let s: Size

a = s! // Error 因为类 Animal 中的成员 feet 是受保护的,所以不能赋值成功

泛型的类型兼容性

interface Empty<T> {}

let x: Empty<number>
let y: Empty<string>

x = y! // OK x 和 y 是兼容的,因为它们的结构使用类型参数时并没有什么不同

当泛型被成员使用时:

interface NotEmpty<T> {
  data: T
}
let x: NotEmpty<number>
let y: NotEmpty<string>

x = y! // Error 

如果没有指定泛型类型的泛型参数,会把所有泛型参数当成 any 类型比较:

let identity = function<T>(x: T): void {
  // ...
}
let reverse = function<U>(y: U): void {
  // ...
}
identity = reverse // OK

学习链接