Typescript学习(五) 初探类型层级

85 阅读7分钟

any和unknown

any

前面, 我们学习了原始类型/字面量类型, 也了解了特殊的void类型, 不过和今天要介绍的any和unknown比起来, 它们都显得过于'普通'了; any即任意的意思, 在Typescript中, any可以存储任意类型的数据; 其他类型的数据, 在noImplicitAny为false的情况下, 可以转隐式为any; 但是, 当noImplicitAny为true的时候, 其他类型的数据, 不得隐式转为any!


  1. 在没有明确标注类型的情况下, 会隐式转为any类型(noImplicitAny:false, 为true则报错)
let a // a:any
function foo (b) {} // b:any
  1. any可以赋给任意类型, 此时其他类型可以隐式转为any(noImplicitAny:false, 为true则报错)

let strs:string = a
let nums:number = a
let bool:boolean = a
let obj:{name:string} = a
let func: () => void = a
  1. 其他类型的数据可以赋值给any, 无论noImplicitAny是什么
a = 'string'
a = 666
a = true
a = () => {}
  1. any类型的属性/方法, 虽然上述操作不受noImplicitAny限制, 但是, 如果想要调用a上的方法或属性, 如果noImplicitAny为true, 将以最后赋值的数据的类型为准! 如果noImplicitAny为false, 则同样无任何限制
// noImplicitAny:true
let a
a.toFixed() // "a" 可能为"为定义"
a = 999
a.toFixed() // 正确!
// noImplicitAny:false
let a
a.b.c.d.e.f // 正确!
  1. 注意, 以上案例都是a声明时未赋值, 而后续赋值; 如果a在声明的时候就已经被赋值, 即使没有明确标注类型, a也不是any了, 而是初始化时被赋予的数据的类型!
let a = 111 // a将被推导为number类型!
a = 'str' // 不能将'string'分配给'number'

any特点小节:

  1. 初始化不赋值, 且不标注类型的变量会被自动隐式转为any类型
  2. any类型在noImplicitAny为true的情况下, 不得赋值给其他类型; 如noImplicitAny为false时, 则可以赋值给其他类型;
  3. 无论noImplicitAny是什么, 其他类型都可以赋值给any类型
  4. noImplicitAny为true的时候, any类型的数据不得调用任意属性/方法, 除非这个类型的数据在前面被赋予了一个值, 而调用的方法存在于这个类型的数据上
  5. 如果一个变量声明的时候没有类型标注, 但是被赋值了, 那么它就不是any类型, 而是自动被赋予值的类型;

总结: any可以看作是类型世界的一个特例, 只有noImplicitAny能够在有限的场景下'治住'它,如果滥用any, 则很容易失去类型保护, 造成很多错误; 所以如非万不得已, 不要轻易使用any; 如果类型不兼容, 可以使用断言, 如果实在不知道这个类型是啥, 可以用unknow来代替;

unknown

unknown和any的不同之处在于, any表示“任意类型”, 而unknown表示“暂时不明的类型”, 两者存在一定的区别:

  1. 无论noImplicitAny是什么, unknown类型数据都不能赋给除any/unknown意外的其他类型;
  2. 无论noImplicitAny是什么, 都不能调用unknown类型数据上的属性/方法;
  3. any在初始化时如果被赋了值, 那它的类型就是那个值的类型, 而非any; unknown如果被赋值, 则其永远是unknown!
let anyData
let unknownData:unknown

// 1.
// 只有noImplicitAny为true的时候, 会报错, 此时anyData为undefined
// noImplicitAny为false时, 则正确
let str:string = anyData 
// 无论noImplicitAny是什么, 都报错, unknownData始终是unknown
let num:number = unknownData

// 2.
anyData.a // noImplicitAny为true的时候, 会报错, 否则正确
unknownData.a // 无论noImplicitAny是什么, 都报错

// 3.初始化anyData2被赋了1, 则其类型转为number类型;
// unknownData2初始化为1, 其类型永远是unknown
let anyData2 = 1
let unknownData2:unknown = 1

anyData2 = '' // 不能将类型"string"分配给类型"number"
unknownData2 = ''

let str2:number = anyData2
let num2:number = unknownData2 // 不能将类型"unknown"分配给类型"number"

never

如果说any/unknown是顶部类型, 那么, never就是底部类型(bottom type), 为什么这么说呢, 因为never中, 不包含任何类型信息! 哪怕连空类型都不是!

type typeData = 'hello' | number | true | never

此时, 将鼠标放到typeData上, 其类型中没有never!

造成这种现象的原因是, never是所有类型的子集, 所以, 它被吸收进了别的类型中,这种现象叫做类型收窄(Type Widening)

never一般用于表示永远不存在值的类型

function test():never {
  throw new Error()
}

当然, 也可以用void来替代

function test():void {
  throw new Error()
}

这样也不会报错, 因为never也是void的子集, 所以这样也不会报错, 但是, 要注意, 两者语义上是不同的, void表示返回空值, 可以是undefined, 但是never表示永远不会返回任何值, 以下代码均正确, 且never都无法取代void!

function test():void {}
function test():void {
  return
}
function test():void {
  return undefined
}

never不可以赋值给任意其他类型的变量, 哪怕any都不行

let a
let neverData:never = a // 不能将类型“any”分配给类型“never”

不过这个特性, 可以帮我们解决一些问题:

function fn (params: string | number | boolean) {
  if (typeof params === 'string') {
    console.log('this is string')
  } else if (typeof params === 'number') {
    console.log('this is number')
  } else if (typeof params === 'boolean') {
    console.log('this is boolean')
  } else {
    let neverData:never = params
    throw new Error('params 为不可知类型')
  }
}

例如以上案例中, 我们定义了一个联合类型的参数 , string | number | boolean, 我们必须为针对每个类型进行特定的处理, 而当判断走到最后的else的时候, params其实是never, 这就是类型收窄, 经过了string、number、boolean、等“层层筛选”之后, params就只剩下never了, 也就是“啥也不是”, 而我们将其赋值给一个never类型的变量neverData, 正常在编译阶段是不会报错的. 但是, 如果此时有一个粗心的开发人员给params加了一个新的类型, 而没有为其增加对应的处理判断, 就会导致params不为never, 一个非never类型的变量是不能赋给never类型的变量的, 因此, 编译阶段就会报错, 也就规避了这个问题了!

断言

前面说了, unknown类型不能进行点操作, 也就是不能获取其属性, 调用其方法, 但是, 有的时候, 我们确实不知道某个变量的具体类型, 而我们又能确定这个变量上肯定有某个方法/属性的时候, 我们该如何处理呢? 答案就是断言, 断言相当于就是告诉编译器: 这里我说它是这个类型, 你不用管了, 出了事我自己负责! 换句话说, 就是强制类型转换, 断言的方式有两种, 一种是使用as关键字转换, 另一种是使用尖角号

let unknownData:unknown
let a = (<string>unknownData).charAt(1)
let b = (unknownData as number).toFixed(2)

断言的好处在于:

  1. 提高代码的可读性, 可以很清晰了解这个类型的结构
  2. 提升开发效率, 对于一些特定的场景, 实在给不出具体的类型, 可以先定一个, 但是, 也不同于any, 它是一个有限自由度的变量;
  3. 对于一些第三方库, 本身类型定义不完整, 因此可以使用断言来绕开此问题;

双重断言

有的类型转换的跨度过大, 导致转换失败, 此时, 需要双重断言, 即先断言为一种通用类型, 再断言为目标类型

let a:string = ''
console.log(a);
(a as any as {name:string}).name

本案例中, string到一个对象, 跨度过大,因此, 可以使用双重断言, 先断言为一个对象

非空断言

主要针对一些可选属性, 当我们需要获取可选属性的时候, ts可能会报错, 提示该属性可能不存在,因此需要非空断言来确保类型校验通过

interface Person {
  name: string;
  pets?: string[]
}

let person:Person
person!.pets!.push('1')