TS关键字any,unknow,never

153 阅读7分钟

了解any,unknow,never等内置标注类型

有时候在我们的ts代码重并不需要十分精确严格的类型标注时,例:consle.log(),这个函数可以接收任何类型的参数,基本类型,对象,数组或者是函数都行,那此时我们该怎么去描述函数参数的类型呢?

为了能够表示“任意类型”,TypeScript 中提供了一个内置类型 any ,来表示所谓的任意类型。此时我们就可以使用 any 作为参数的类型:

log(message?: any, ...optionalParams: any[]): void

在这里,一个被标记为 any 类型的参数可以接受任意类型的值。除了message是any类型以外,optionalParams表示该函数除message以外接收的所有参数是函数的rest 参数(JavaScript 中的 rest 参数允许你将不定数量的参数表示为一个数组。当函数被调用时,rest 参数会将所有剩余的参数收集到一个数组中。使用 rest 参数时,你可以在函数声明中使用三个点号 ... 来指示这是一个 rest 参数,其语法形式为 ...变量名。这个变量将成为一个包含了函数接收到的所有剩余参数的数组。),any[]表示这个数组中所有元素都是any类型,表示函数除了message以外所有参数也都是any类型,除了显示的声明any类型在某些情况下你的变量/参数也会被隐式地推导为 any,比如使用 let 声明一个变量但不提供初始值,以及不为函数参数提供类型标注:

// any
let foo;

// foo、bar 均为 any
function func(foo, bar){}

以上的函数声明在 tsconfig 中启用了 noImplicitAny 时会报错,你可以显式为这两个参数指定 any 类型,或者暂时关闭这一配置(不推荐)。而 any 类型的变量几乎无所不能,它可以在声明后再次接受任意类型的值,同时可以被赋值给任意其它类型的变量:

// 被标记为 any 类型的变量可以拥有任意类型的值
let anyVar: any = "linbudu";

anyVar = false;
anyVar = "linbudu";
anyVar = {
  site: "juejin"
};

anyVar = () => { }

// 标记为具体类型的变量也可以接受任何 any 类型的值
const val1: string = anyVar;
const val2: number = anyVar;
const val3: () => {} = anyVar;
const val4: {} = anyVar;

你可以在 any 类型变量上任意地进行操作,包括赋值、访问、方法调用等等,此时可以认为类型推导与检查是被完全禁用的:

let anyVar: any = null;


//此函数调用不会报错 但是null没有foo这个函数
anyVar.foo.bar.baz();
anyVar[0][1][2].prop1;

而 any 类型的主要意义,其实就是为了表示一个无拘无束的“任意类型”,它能兼容所有类型,也能够被所有类型兼容。这意味着你使用any标注的变量,此时跳过了类型检查.

虽然any能有效帮助我们解决一些位置变量的类型标注但是也不能去滥用他,注意:

  • 如果是类型不兼容报错导致你使用 any,考虑用类型断言替代,我们下面就会开始介绍类型断言的作用。
  • 如果是类型太复杂导致你不想全部声明而使用 any,考虑将这一处的类型去断言为你需要的最简类型。如你需要调用 foo.bar.baz(),就可以先将 foo 断言为一个具有 bar 方法的类型。
  • 如果你是想表达一个未知类型,更合理的方式是使用 unknown。(再次使用时需断言)

unknown 类型和 any 类型有些类似,一个 unknown 类型的变量可以再次赋值为任意其它类型,但只能赋值给 any 与 unknown 类型的变量:

let unknownVar: unknown = "linbudu";

unknownVar = false;
unknownVar = "linbudu";
unknownVar = {
  site: "juejin"
};

unknownVar = () => { }

const val1: string = unknownVar; // Error
const val2: number = unknownVar; // Error
const val3: () => {} = unknownVar; // Error
const val4: {} = unknownVar; // Error

const val5: any = unknownVar;
const val6: unknown = unknownVar;

简单地说,any 放弃了所有的类型检查,而 unknown 并没有。这一点也体现在对 unknown 类型的变量进行属性访问时:

let unknownVar: unknown;

unknownVar.foo(); // 报错:对象类型为 unknown

要对 unknown 类型进行属性访问,需要进行类型断言**(别急 , 我知道你很急但你先别急, 马上就讲类型断言!)**,即“虽然这是一个未知的类型,但我跟你保证它在这里就是这个类型!”:

let unknownVar: unknown;

//将unknow类型断言成一个有foo方法的字面量类型
(unknownVar as { foo: () => {} }).foo();

在类型未知的情况下,更推荐使用 unknown 标注。这相当于你使用额外的心智负担保证了类型在各处的结构,后续重构为具体类型时也可以获得最初始的类型信息,同时还保证了类型检查的存在。当然,unknown 用起来很麻烦,一堆类型断言写起来可不太好看。归根结底,到底用哪个完全取决于你自己,毕竟语言只是工具嘛。

虚无的 never 类型

是不是有点不好理解?我们看一个联合类型的例子就能 get 到一些了。

type UnionWithNever = "linbudu" | 599 | true | void | never;

将鼠标悬浮在类型别名之上,你会发现这里显示的类型是"linbudu" | 599 | true | void。never 类型被直接无视掉了,而 void 仍然存在。这是因为,void 作为类型表示一个空类型,就像没有返回值的函数使用 void 来作为返回值类型标注一样,void 类型就像 JavaScript 中的 null 一样代表“这里有类型,但是个空类型”。

而 never 才是一个“什么都没有”的类型,它甚至不包括空的类型,严格来说,never 类型不携带任何的类型信息,因此会在联合类型中被直接移除,比如我们看 void 和 never 的类型兼容性:

declare let v1: never;
declare let v2: void;

v1 = v2; // X 类型 void 不能赋值给类型 never

v2 = v1;

在编程语言的类型系统中,never 类型被称为 Bottom Type,是整个类型系统层级中最底层的类型。和 null、undefined 一样,它是所有类型的子类型,但只有 never 类型的变量能够赋值给另一个 never 类型变量。

通常我们不会显式地声明一个 never 类型,它主要被类型检查所使用。但在某些情况下使用 never 确实是符合逻辑的,比如一个只负责抛出错误的函数:

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

在类型流的分析中,一旦一个返回值类型为 never 的函数被调用,那么下方的代码都会被视为无效的代码(即无法执行到):

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

function foo (input:number){
  if(input > 1){
    justThrow();
    // 等同于 return 语句后的代码,即 Dead Code
    const name = "linbudu";
  }
}

我们也可以显式利用它来进行类型检查,即上面在联合类型中 never 类型神秘消失的原因。假设,我们需要对一个联合类型的每个类型分支进行不同处理:

declare const strOrNumOrBool: string | number | boolean;

if (typeof strOrNumOrBool === "string") {
  console.log("str!");
} else if (typeof strOrNumOrBool === "number") {
  console.log("num!");
} else if (typeof strOrNumOrBool === "boolean") {
  console.log("bool!");
} else {
  throw new Error(`Unknown input type: ${strOrNumOrBool}`);
}

如果我们希望这个变量的每一种类型都需要得到妥善处理,在最后可以抛出一个错误,但这是运行时才会生效的措施,是否能在类型检查时就分析出来?

实际上,由于 TypeScript 强大的类型分析能力,每经过一个 if 语句处理,strOrNumOrBool 的类型分支就会减少一个(因为已经被对应的 typeof 处理过)。而在最后的 else 代码块中,它的类型只剩下了 never 类型,即一个无法再细分、本质上并不存在的虚空类型。在这里,我们可以利用只有 never 类型能赋值给 never 类型这一点,来巧妙地分支处理检查:

if (typeof strOrNumOrBool === "string") {
    // 一定是字符串!
  strOrNumOrBool.charAt(1);
} else if (typeof strOrNumOrBool === "number") {
  strOrNumOrBool.toFixed();
} else if (typeof strOrNumOrBool === "boolean") {
  strOrNumOrBool === true;
} else {
  const _exhaustiveCheck: never = strOrNumOrBool;
  throw new Error(`Unknown input type: ${_exhaustiveCheck}`);
}

假设某个粗心的同事新增了一个类型分支,strOrNumOrBool 变成了 strOrNumOrBoolOrFunc,却忘记新增对应的处理分支,此时在 else 代码块中就会出现将 Function 类型赋值给 never 类型变量的类型错误。这实际上就是利用了类型分析能力与 never 类型只能赋值给 never 类型这一点,来确保联合类型变量被妥善处理。

其实never类型除了主动声明以外,有的情况其实也会出现never类型报错:

const arr = [];

arr.push("linbudu"); // 类型“string”的参数不能赋给类型“never”的参数。

此时这个未标明类型的数组被推导为了 never[] 类型,这种情况仅会在你启用了 strictNullChecks 配置,同时禁用了 noImplicitAny 配置时才会出现。解决的办法也很简单,为这个数组声明一个具体类型即可.