Skip to content

any

TypeScript 中提供了一个内置类型 any ,来表示所谓的任意类型。 有时候,这也是必要的,比如console.log(), 对于 log 函数,我们就可以使用 any 作为参数的类型

typescript
log(message?: any, ...optionalParams: any[]): void
// 我们可以使用任意参数类型来调用log方法

除了显式的标记一个变量或参数为 any,在某些情况下你的变量/参数也会被隐式地推导为 any。比如使用 let 声明一个变量但不提供初始值,以及不为函数参数提供类型标注:

typescript
// any
let foo;

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

上述代码,在 tsconfig 中启用了 noImplicitAny 时会报错,我们可以显式为这两个参数指定 any 类型,或者暂时关闭这一配置(不推荐)。

typescript
// 被标记为 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 类型的变量标记,可以认为类型推导与检查是被完全禁用的:

typescript
let anyVar: any = null;

anyVar.foo.bar.baz();
anyVar[0][1][2].prop1;
// 不会报错

any 类型的主要意义,其实就是为了表示一个无拘无束的“任意类型”,它能兼容所有类型,也能够被所有类型兼容。这一作用其实也意味着类型世界给我们开了一个外挂,无论什么时候,我们都可以使用 any 类型跳过类型检查。 不建议乱用 any,导致 TypeScript 变成 AnyScript。

Tips

  1. 如果是类型不兼容报错导致使用 any,考虑用类型断言替代
  2. 如果想表达一个未知类型,更合理的方式是使用 unknown

unknown

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

typescript
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

// unknown 只能赋值给any或者unknown
const val5: any = unknownVar;
const val6: unknown = unknownVar;

any 就像是 “我身化万千无处不在” ,所有类型都把它当自己人。

unknown 就像是 “我虽然身化万千,但我坚信我在未来的某一刻会得到一个确定的类型” ,只有 any 和 unknown 自己把它当自己人。

类型断言

要对 unknown 类型进行属性访问,需要进行类型断言,即“虽然这是一个未知的类型,但我跟你保证它在这里就是这个类型!”:

typescript
let unknownVar: unknown;

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

// 断言
let unknownVar: unknown;

(unknownVar as { foo: () => {} }).foo();

在类型未知的情况下,更推荐使用 unknown 标注。eg:

typescript
function myFunc(param: unknown) {
  param.forEach((element) => {}); // X “param”的类型为“未知”。
}

如果要将 unknown 类型的变量断言到数组类型,我们可以这么写:

typescript
function myFunc(param: unknown) {
  (param as unknown[]).forEach((element) => {});
}

never

内置类型 never 描述一个“什么都没有”的类型。相比另一个“什么都没有”的类型,void。never 还要更加空白一些。

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

将鼠标悬浮在类型别名之上,会发现这里显示的类型是"linbudu" | 599 | true | void。never 类型被直接无视掉了,而 void 仍然存在。这是因为,void 作为类型表示一个空类型,就像没有返回值的函数使用 void 来作为返回值类型标注一样,void 类型就像 JavaScript 中的 null 一样代表“这里有类型,但是个空类型”。 而 never 才是一个“什么都没有”的类型,它甚至不包括空的类型,严格来说,never 类型不携带任何的类型信息,因此会在联合类型中被直接移除,比如我们看 void 和 never 的类型兼容性:

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

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

v2 = v1;

在编程语言的类型系统中,never 类型被称为 Bottom Type,是整个类型系统层级中最底层的类型。和 null、undefined 一样,它是所有类型的子类型,但只有 never 类型的变量能够赋值给另一个 never 类型变量。 通常我们不会显式地声明一个 never 类型,它主要被类型检查所使用。但在某些情况下使用 never 确实是符合逻辑的,比如一个只负责抛出错误的函数:

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

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

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

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

显示利用做类型检测

假设,我们需要对一个联合类型的每个类型分支进行不同处理:

typescript
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 类型这一点,来巧妙地分支处理检查:

typescript
declare const strOrNumOrBool: string | number | boolean;

if (typeof strOrNumOrBool === "string") {
  // 一定是字符串!
  strOrNumOrBool.charAt(1);
} else if (typeof strOrNumOrBool === "number") {
  strOrNumOrBool.toFixed();
} else if (typeof strOrNumOrBool === "boolean") {
  strOrNumOrBool === true;
} else {
  // 只能是never类型了,再利用never赋值给never
  const _exhaustiveCheck: never = strOrNumOrBool;
  throw new Error(`Unknown input type: ${_exhaustiveCheck}`);
}

TypeScript 中 Top Type(any / unknown) 与 Bottom Type(never)

类型断言

TypeScript 中的类型断言是一种手动指定变量或对象属性的类型的方法。它可以帮助我们在某些情况下规避类型检查器的限制,但需要谨慎使用,因为滥用类型断言会带来一些风险。

它其实就是一个将变量的已有类型更改为新指定类型的操作,它的基本语法是 as NewType,我们可以将 any / unknown 类型断言到一个具体的类型。

typescript
let unknownVar: unknown;

(unknownVar as { foo: () => {} }).foo();

还可以 as 到 any 来为所欲为,跳过所有的类型检查:

typescript
const str: string = "linbudu";

// 不要使用!!
(str as any).func().foo().prop;

也可以在联合类型中断言一个具体的分支:

typescript
function foo(union: string | number) {
  if ((union as string).includes("linbudu")) {
  }

  if ((union as number).toFixed() === "599") {
  }
}

但是类型断言的正确使用方式是,在 TypeScript 类型分析不正确或不符合预期时,将其断言为此处的正确类型:

typescript
interface IFoo {
  name: string;
}

declare const obj: {
  foo: IFoo;
};

const { foo = {} as IFoo } = obj;
// 这里,
// 通过类型断言 {} as IFoo 将一个空对象 {} 断言为 IFoo 类型,
// 作为 foo 属性的默认值。

这里从 {} 字面量类型断言为了 IFoo 类型,即为解构赋值默认值进行了预期的类型断言。当然,更严谨的方式应该是定义为 Partial<IFoo> 类型,即 IFoo 的属性均为可选的。 下面是使用 Partial<IFoo> 作为默认值类型的示例:

typescript
interface IFoo {
  name: string;
  age?: number; // 可选属性
}

declare const obj: { foo?: IFoo };

// 使用 Partial<IFoo> 作为默认值的类型
const { foo = {} as Partial<IFoo> } = obj;

// 现在 foo 被推导为 { name?: string, age?: number } 类型
console.log(foo.name); // 可选属性,不会报错
console.log(foo.age); // 同样不会报错

使用 Partial<IFoo> 作为默认值的类型,这意味着默认值 {} 被推导为一个所有属性都是可选的对象类型 { name?: string, age?: number }。 使用 Partial 的好处是,我们不需要手动列出所有属性并将它们标记为可选,TypeScript 会自动为我们完成这个操作

语法

TypeScript 中有两种类型断言的语法形式:

typescript
// as 语法
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

// 尖括号语法
let someValue: any = "this is a string";
// 将someValue断言为string类型
let strLength: number = (<string>someValue).length;

尖括号语法虽然书写更简洁,但效果一致,只是在 TSX 中尖括号断言并不能很好地被分析出来。你也可以通过 TypeScript ESLint 提供的 consistent-type-assertions 规则来约束断言风格。

何时使用类型断言

  • 当你比 TypeScript 更了解某个值的详细信息时
  • 当你从一个第三方代码库中引用了一个类型很松散的值时
  • 当你手动模拟一个从 JavaScript 中获取的值时
typescript
// 从 DOM 中获取的值默认为 any 类型
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;

// 从 JSON 数据中获取的数据默认为 any 类型
const response = await fetch("/api/data.json");
const data = (await response.json()) as Data;

双重断言

如果在使用类型断言时,原类型与断言类型之间差异过大,也就是指鹿为马太过离谱,离谱到了指鹿为霸王龙的程度,TypeScript 会给我们一个类型报错:

typescript
const str: string = "hello world";

// 从 X 类型 到 Y 类型的断言可能是错误的,blabla
(str as { handler: () => {} }).handler();

此时它会提醒你先断言到 unknown 类型,再断言到预期类型,就像这样:

typescript
const str: string = "hello world";

(str as unknown as { handler: () => {} }).handler();

// 使用尖括号断言
(<{ handler: () => {} }>(<unknown>str)).handler();

这是因为你的断言类型和原类型的差异太大,需要先断言到一个通用的类,即 any / unknown。这一通用类型包含了所有可能的类型,因此断言到它从它断言到另一个类型差异不大。

非空断言

非空断言其实是类型断言的简化,它使用 ! 语法,即 obj!.func!().prop 的形式标记前面的一个声明一定是非空的(实际上就是剔除了 null 和 undefined 类型),比如这个例子:

typescript
declare const foo: {
  func?: () => {
    prop?: number | null;
  };
};

foo.func().prop.toFixed();

如果不管三七二十一地坚持调用,想要解决掉类型报错就可以使用非空断言:

typescript
foo.func!().prop!.toFixed();
// 注意如果是方法 ! 在 () 前面 =>   obj.func!().prop!.toFixed()

其应用位置类似于可选链:

typescript
foo.func?.().prop?.toFixed();

不同的是,非空断言的运行时仍然会保持调用链,因此在运行时可能会报错。而可选链则会在某一个部分收到 undefined 或 null 时直接短路掉,不会再发生后面的调用

代码提示辅助

typescript
interface IStruct {
  foo: string;
  bar: {
    barPropA: string;
    barPropB: number;
    barMethod: () => void;
    baz: {
      handler: () => Promise<void>;
    };
  };
}

如果我们想基于这个结构随便实现一个对象

typescript
const obj: IStruct = {};

这个时候等待你的是一堆类型报错,你必须规规矩矩地实现整个接口结构才可以。但如果使用类型断言,我们可以在保留类型提示的前提下,不那么完整地实现这个结构:

typescript
// 这个例子是不会报错的
const obj = <IStruct>{
  bar: {
    baz: {},
  },
};

// Error
const obj = <IStruct>{
  foo: 400, // 依然会有类型检测
};