any,unknown ,never 类型
三、any,unknown ,never 类型
1、any 类型
1.1、基本含义
any 类型表示没有任何限制,该类型的变量可以赋予任意类型的值。
let x:any;
x = 1; // 正确
x = 'foo'; // 正确
x = true; // 正确
let x:any = 'hello';
x(1) // 不报错
x.foo = 100; // 不报错
变量类型一旦设为any,TypeScript 实际上会关闭这个变量的类型检查。只要句法正确,都不会报错。
由于这个原因,应该尽量避免使用any类型,否则就失去了使用 TypeScript 的意义。
any类型主要适用以下两个场合:
- 需要关闭某些变量的类型检查
- 为了适配以前老的 JavaScript 项目,让代码快速迁移到 TypeScript,可以把变量类型设为any
1.2、类型推断问题
对于开发者没有指定类型、TypeScript 必须自己推断类型的那些变量,如果无法推断出类型,TypeScript 就会认为该变量的类型是any
function add(x, y) {
  return x + y;
}
add(1, [1, 2, 3]) // 不报错
上面示例中,函数add()的参数变量x和y,都没有足够的信息,TypeScript 无法推断出它们的类型,就会认为这两个变量和函数返回值的类型都是any。以至于后面就不再对函数add()进行类型检查了,怎么用都可以。
TypeScript 提供了一个编译选项noImplicitAny,打开该选项,只要推断出any类型就会报错。
$ tsc --noImplicitAny app.ts # 会报错
这里有一个特殊情况,即使打开了noImplicitAny,使用let和var命令声明变量,但不赋值也不指定类型,是不会报错的。
var x; // 不报错 TypeScript 会推断它们的类型为any。这时即使打开了noImplicitAny,也不会报错。
let y; // 不报错
相关信息
建议使用let和var声明变量时,如果不赋值,就一定要显式声明类型,否则可能存在安全隐患。
const命令没有这个问题,因为 JavaScript 语言规定const声明变量时,必须同时进行初始化(赋值)
const命令声明的x是不能改变值的,声明时必须同时赋值,否则报错,所以它不存在类型推断为any的问题。
const x; // 报错
1.3、污染问题
any类型除了关闭类型检查,还有一个很大的问题,就是它会“污染”其他变量。它可以赋值给其他任何类型的变量(因为没有类型检查),导致其他变量出错。
let x:any = 'hello';
let y:number;
y = x; // 不报错
y * 123 // 不报错
y.toFixed() // 不报错
相关信息
污染其他具有正确类型的变量,把错误留到运行时,这就是不宜使用any类型的另一个主要原因。
2、unknown 类型
为了解决any类型“污染”其他变量的问题,TypeScript 3.0 引入了unknown类型。它与any含义相同,表示类型不确定,可能是任意类型,但是它的使用有一些限制,不像any那样自由,可以视为 严格版的any。
unknown跟any的相似之处,在于所有类型的值都可以分配给unknown类型。
let x:unknown;
x = true; // 正确
x = 42; // 正确
x = 'Hello World'; // 正确
unknown类型跟any类型的不同之处在于,它不能直接使用。主要有以下几个限制。
- unknown类型的变量,不能直接赋值给其他类型的变量(除了- any类型和- unknown类型)。- let v:unknown = 123; let v1:boolean = v; // 报错 let v2:number = v; // 报错- 变量 - v是- unknown类型,赋值给- any和- unknown以外类型的变量都会报错,这就避免了污染问题,从而克服了- any类型的一大缺点。
- 不能直接调用 - unknown类型变量的方法和属性。- let v1:unknown = { foo: 123 }; v1.foo // 报错 let v2:unknown = 'hello'; v2.trim() // 报错 let v3:unknown = (n = 0) => n + 1; v3() // 报错- 直接调用 - unknown类型变量的属性和方法,或者直接当作函数执行,都会报错。
- 再次, - unknown类型变量能够进行的运算是有限的- 只能进行比较运算(运算符 - ==、- ===、- !=、- !==、- ||、- &&、- ?)、取反运算(运算符- !)、- typeof运算符和- instanceof运算符这几种,其他运算都会报错。- let a:unknown = 1; a + 1 // 报错 a === 1 // 正确
- 使用 - unknown类型变量- 只有经过“类型缩小”, - unknown类型变量才可以使用。所谓“类型缩小”,就是缩小- unknown变量的类型范围,确保不会出错。- let a:unknown = 1; if (typeof a === 'number') { let r = a + 10; // 正确 } let s:unknown = 'hello'; if (typeof s === 'string') { s.length; // 正确 }- 上面示例中, - unknown类型的变量- a经过**- typeof**运算以后,能够确定实际类型是- number,就能用于加法运算了。这就是“类型缩小”,即将一个不确定的类型缩小为更明确的类型。- unknown可以看作是更安全的- any。一般来说,凡是需要设为- any类型的地方,通常都应该优先考虑设为- unknown类型。- 在集合论上, - unknown也可以视为所有其他类型(除了- any)的全集,所以它和- any一样,也属于 TypeScript 的顶层类型。
3、never 类型
为了保持与集合论的对应关系,以及类型运算的完整性,TypeScript 还引入了“空类型”的概念,即该类型为空,不包含任何值。
由于不存在任何属于“空类型”的值,所以该类型被称为never,即不可能有这样的值。
let x:never;
上面示例中,变量x的类型是never,就不可能赋给它任何值,否则都会报错。
never类型的使用场景,主要是在一些类型运算之中,保证类型运算的完整性,详见后面章节。
 另外,不可能返回值的函数,返回值的类型就可以写成never,详见《函数》一章。
如果一个变量可能有多种类型(即联合类型),通常需要使用分支处理每一种类型。这时,处理所有可能的类型之后,剩余的情况就属于never类型。
function fn(x:string|number) {
  if (typeof x === 'string') {
    // ...
  } else if (typeof x === 'number') {
    // ...
  } else {
    x; // never 类型
  }
}
上面示例中,参数变量x可能是字符串,也可能是数值,判断了这两种情况后,剩下的最后那个else分支里面,x就是never类型了。
never类型的一个重要特点是,可以赋值给任意其他类型。
function f():never {
  throw new Error('Error');
}
let v1:number = f(); // 不报错
let v2:string = f(); // 不报错
let v3:boolean = f(); // 不报错
上面示例中,函数f()会抛错,所以返回值类型可以写成never,即不可能返回任何值。各种其他类型的变量都可以赋值为f()的运行结果(never类型)。
为什么never类型可以赋值给任意其他类型呢?这也跟集合论有关,空集是任何集合的子集。TypeScript 就相应规定,任何类型都包含了never类型。因此,never类型是任何其他类型所共有的,TypeScript 把这种情况称为“底层类型”(bottom type)。
总之,TypeScript 有两个“顶层类型”(any和unknown),但是“底层类型”只有never唯一一个。