在TypeScript中,any 类型是一个非常强大的类型。它允许你将一个值当作在JavaScript中一样来处理,而不是在TypeScript中。这意味着它禁用了TypeScript的所有特性,包括类型检查、自动补全和安全性。

const myFunction = (input: any) => {
  input.someMethod();
};

myFunction("abc"); // This will fail at runtime!

在实际项目中,any 可能会造成以下几个问题:

  1. 丧失类型安全性:使用 any 会导致 TypeScript 不再检查你的代码是否存在潜在的错误,这与引入 TypeScript 的初衷背道而驰。
  2. 难以维护:当代码库中充斥着大量的 any 类型时,调试和维护变得更加困难。你无法通过类型系统获得代码的准确信息。
  3. 隐藏错误:因为 any 允许你调用不存在的属性或方法,这可能会导致隐藏的 bug 只有在运行时才暴露出来,而不是在编译时发现。

在大多数情况下,any 类型被认为是bad code。并会使用eslint来避免使用any。然而,有一些情况使用 any 却是正确的选择。

类型参数约束

假设我们要在TypeScript中实现一个ReturnType的工具方法,该方法接受一个函数类型,并返回它的返回值类型。

我们需要创建一个泛型类型,这个泛型类型接受一个函数类型作为其类型参数。如果我们限制自己不使用任何特定的类型(例如any),我们可能会选择使用unknown类型。unknown 是比 any 更加严格和安全的类型。它允许你存储任何类型的值,但在你使用该值之前必须进行类型检查,不能随意调用属性或方法。

type ReturnType<T extends (...args: unknown[]) => unknown> = T extends (...args: unknown[]) => infer R ? R : never;
  • ReturnType<T>: 这里定义了一个泛型类型ReturnType,它接受一个类型参数T。
    类型约束:
  • T extends (...args: unknown[]) => unknown: 这个约束指定T必须是一个函数类型,这个函数接受一个参数列表(每个参数都是unknown类型),并返回一个unknown类型的值。
    条件类型:
  • T extends (...args: unknown[]) => infer R ? R : never: 这是一个条件类型表达式,它使用了infer关键字来推断函数返回的具体类型。

    • infer R: 这部分尝试从满足(...args: unknown[]) => unknown类型的函数中推断出返回类型R。
      ? R: 如果推断成功,那么ReturnType<T>的结果是推断出的返回类型R。
    • : never: 如果推断失败(例如,如果T不是一个函数类型或者函数的返回类型不是unknown),则ReturnType<T>的结果是never类型,表示这个类型不存在任何值。

但一旦我们添加一个参数,它就失效了。提示无法把unknown赋值给string。

实际上,如果我们参数是unknown的话,它确实是生效的。

只接受unknown作为参数的效果显然不是我们想要的。我们希望它对任何函数都有效。这时候就不得不使用any[]作为类型参数约束:

type ReturnType<T extends (...args: any[]) => any> =
  T extends (...args: any[]) => infer R ? R : never;

const myFunction = (input: string) => {
  console.log("Hey!");
};

type Result = ReturnType<typeof myFunction>;

现在它按预期工作了。我们不在乎函数接受什么类型——它可以是任意类型。因为我们本来的目的就是创建一个不严格的类型,所以这是安全的。

从通用函数中返回条件类型

在某些情况下,TypeScript的类型缩小能力可能不足以满足需求。例如,你可能希望创建一个函数,根据条件返回不同的类型。

const youSayGoodbyeISayHello = (
  input: "hello" | "goodbye"
) => {
  if (input === "goodbye") {
    return "hello";
  } else {
    return "goodbye";
  }
};

const result = youSayGoodbyeISayHello("hello");

在这里,result会被推断成"hello" | "goodbye"。但我们传进去的是hello嗄,hello返回的应该就是goodbye。这显然是不准确的。我们可以这样子改造一下:

const youSayGoodbyeISayHello = <
  TInput extends "hello" | "goodbye"
>(
  input: TInput
): TInput extends "hello" ? "goodbye" : "hello" => {
  if (input === "goodbye") {
    return "hello" as TInput extends "hello"
      ? "goodbye"
      : "hello";
  } else {
    return "goodbye" as TInput extends "hello"
      ? "goodbye"
      : "hello";
  }
};

我们为函数的返回类型添加了一个条件类型,它反映了我们的运行时逻辑。通过对结果做类型转换,实现我们想要的结果。但这样子做会有一个弊端,每个类型都要写一次。那有没有更好的方式呢?使用any会显得更通用。

const youSayGoodbyeISayHello = <
  TInput extends "hello" | "goodbye"
>(
  input: TInput
): TInput extends "hello" ? "goodbye" : "hello" => {
  if (input === "goodbye") {
    return "hello" as any;
  } else {
    return "goodbye" as any;
  }
};

如果不使用 any,TypeScript可能无法正确地将条件类型与运行时逻辑相匹配,导致类型错误。

结论

一个问题仍然存在:你应该禁止any吗?答案应该是肯定的。您应该打开ESLint规则,以防止其使用,并且您应该尽可能避免使用它。

然而,有些情况下需要any。他们值得使用eslint-disable来绕过它们。

本文由mdnice多平台发布


Miniwa
29 声望1 粉丝