TypeScript(一):any类型、unknown类型、never类型

144 阅读8分钟

any 类型

基本概念

any类型表示没有任何限制,该类型的变量可以赋予任意类型的值

let x : any;

x = 1; // 正确
x = "a"; // 正确
x = null: // 正确

上面的x变量声明为any,就可以赋予任何类型的值;

一旦将变量声明为any,typescript会关闭对该变量的类型检查,即使有明显的类型错误,只要语法没有错误,都不会报错

let x : any = "hello";

x(1); // 不报错

x.foo; // 不报错

在上面的代码中,x为字符串类型,但是可以把它作为函数调用,或者作为对象访问其中的属性,typescript都不会报错,原因就是声明xany类型,typescript不会对该变量进行类型检查。

由于这个原因,在实际开发中我们应该避免使用any,否则就失去了typescript的意义。

在实际开发中,any适用于以下场景:

  • 由于某些特殊原因,需要关闭TypeScript的类型检查,可以将该变量设置为any
  • 为了适配老的项目代码,将js代码迁移到ts文件中,可以将变量设置为any。特别是年代久远的大型项目,包括历史代码,很难为每一段代码做适配,这时可以讲一些复杂的变量设置为any,TypeScript编译时就不会报错。

总之TypeScript认为只要开发者使用了any,则代表开发者想自己处理这些类型,TypeScript会关闭对这些变量的类型检查。

从集合论的角度来看,any类型可以代表其他所有类型的全集,包含了一切可能的类型。

TypeScript将这种类型称为“顶层类型“(top type),意为涵盖了所有下层。

类型判断问题

对于开发者没有指定类型的变量,TypeScript必须自己推断类型的变量,如何无法判断出变量类型,TypeScript会认为该变量类型为any

function add (x, y) {
    return x + y;
}

add(1, [1, 2, 3]); // 不报错

上面的示例中,函数add接受两个参数xy,此时没有足够的信息,TypeScript无法推导出x和y的类型,就会认为这两个变量和函数返回值都为any,所以不会对函数add进行类型检查了。

这显然是很糟糕的情况,所以对于那些类型不明显的变量,一定要显式声明类型,防止被推断为any

TypeScript 提供了一个编译选项noImplicitAny,打开该选项,只要推断出any类型就会报错。

image.png
这里有一个特殊情况,即使打开了noImplicitAny,使用letvar命令声明变量,但不赋值也不指定类型,是不会报错的。

var x; // 不报错
let y; // 不报错

上面示例中,变量xy声明时没有赋值,也没有指定类型,TypeScript 会推断它们的类型为any。这时即使打开了noImplicitAny,也不会报错。

let x;

x = 123;
x = { foo: 'hello' };

上面示例中,变量x的类型推断为any,但是不报错,可以顺利通过编译。

由于这个原因,建议使用letvar声明变量时,如果不赋值,就一定要显式声明类型,否则可能存在安全隐患。

const命令没有这个问题,因为 JavaScript 语言规定const声明变量时,必须同时进行初始化(赋值)。

变量污染

any类型除了关闭类型检查,还有一个很大的问题,就是它会“污染”其他变量。它可以赋值给其他任何类型的变量(因为没有类型检查),导致其他变量出错。

let x:any = 'hello';
let y:number;

y = x; // 不报错

y * 123 // 不报错
y.toFixed() // 不报错

上面示例中,变量x的类型是any,实际的值是一个字符串。变量y的类型是number,表示这是一个数值变量,但是它被赋值为x,这时并不会报错。然后,变量y继续进行各种数值运算,TypeScript 也检查不出错误,问题就这样留到运行时才会暴露。

污染其他具有正确类型的变量,把错误留到运行时,这就是不宜使用any类型的另一个主要原因。

unknown 类型

为了解决any污染其他变量的问题,TypeScript在3.0引入了unknown类型。它与any的含义相同,表示类型的不确定,可以是任何类型,但是它有一些使用限制,不像any一样自由,可以称为严格版的any.

unknownany的相似之处,在于任何类型都可以赋值给unknown

let x : unknown;

x = 123; // 正确
x = "a"; // 正确
x = { foo : 1 }; // 正确

上面示例中,变量x的类型是unknown,可以赋值为各种类型的值。这与any的行为一致。

unknown类型跟any类型的不同之处在于,它不能直接使用。主要有以下几个限制。

首先,unknown类型的变量,不能直接赋值给其他类型的变量(除了any类型和unknown类型)。

let x : unknown = 123;

let y : string = x; // 报错
let z : number = x; // 报错

上面示例中,变量xunknown类型,赋值给anyunknown以外类型的变量都会报错,这就避免了污染问题,从而克服了any类型的一大缺点。

其次,不能直接调用unknown类型变量的方法和属性。

let x : unknown = { foo: 123 };
x.foo  // 报错

let y : unknown = 'hello';
y.trim() // 报错

let z : unknown = (n = 0) => n + 1;
z() // 报错

上面示例中,直接调用unknown类型变量的属性和方法,或者直接当作函数执行,都会报错。

再次,unknown类型变量能够进行的运算是有限的,只能进行比较运算(运算符=====!=!==||&&?)、取反运算(运算符!)、typeof运算符和instanceof运算符这几种,其他运算都会报错。

let a:unknown = 1;

a + 1 // 报错
a === 1 // 正确

上面示例中,unknown类型的变量a进行加法运算会报错,因为这是不允许的运算。但是,进行比较运算就是可以的。

那么,怎么才能使用unknown类型变量呢?

答案是只有经过“类型缩小”,unknown类型变量才可以使用。所谓“类型缩小”,就是缩小unknown变量的类型范围,确保不会出错。

let a:unknown = 1;

if (typeof a === 'number') {
  let r = a + 10; // 正确
}

上面示例中,unknown类型的变量a经过typeof运算以后,能够确定实际类型是number,就能用于加法运算了。这就是“类型缩小”,即将一个不确定的类型缩小为更明确的类型。

下面是另一个例子。

let s:unknown = 'hello';

if (typeof s === 'string') {
  s.length; // 正确
}

上面示例中,确定变量s的类型为字符串以后,才能调用它的length属性。

这样设计的目的是,只有明确unknown变量的实际类型,才允许使用它,防止像any那样可以随意乱用,“污染”其他变量。类型缩小以后再使用,就不会报错。

总之,unknown可以看作是更安全的any。一般来说,凡是需要设为any类型的地方,通常都应该优先考虑设为unknown类型。

在集合论上,unknown也可以视为所有其他类型(除了any)的全集,所以它和any一样,也属于 TypeScript 的顶层类型。

never 类型

为了保持与集合论的对应关系,以及类型运算的完整性,TypeScript 还引入了“空类型”的概念,即该类型为空,不包含任何值。

由于不存在任何属于“空类型”的值,所以该类型被称为never,即不可能有这样的值。

let x : never; 

上面示例中,变量x的类型是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 有两个“顶层类型”(anyunknown),但是“底层类型”只有never唯一一个。