打字稿从不推断方法参数

时间:2021-05-02 22:11:49

标签: typescript

我有一个包含方法 f 的对象数组。此方法接收一个字符串或一个数字,并始终返回一个字符串。我在下面的代码中定义了它的类型,但是 Typescript 正在推断 f: (value: never) => string,所以我不能调用它。

我该如何完成这项工作?

type ObjType = {
  f: ((value: string) => string) | ((value: number) => string)
}

const objArray: Array<ObjType> = [
  { f: (value: string) => value },
  { f: (value: number) => value.toFixed(2) }
]

console.log(objArray[0].f('1'))
console.log(objArray[1].f(1))

1 个答案:

答案 0 :(得分:3)

如果编译器只知道值 obj 的类型为 ObjType,则没有值 x 对其调用 obj.f(x) 是类型安全的。编译器知道 obj.f要么接受string它将接受number。但它不知道是哪一个。唯一安全的 x 应该是两者string numberstring & number。但是没有这样的值; stringnumberintersectionnever。这就是编译器推断 never 作为 f 的方法参数的原因。

annotated objArray 作为类型 Array<ObjType>。这样做显式地丢弃了编译器可能拥有的有关数组文字的特定元素的任何信息;从那时起,它只知道 objArray 的每个元素都是一个 ObjType。因此它不会让您对其元素调用 f() 方法。一般来说,如果你注释一个像 const variable: Type = ... 这样的变量,并且如果 Type 本身不是一个 union(并且 Array<ObjType> 不是一个联合),编译器只会知道variableType 类型;它不会跟踪任何额外的信息。


如果您希望能够对 f 的元素调用 objArray 方法,则必须向编译器提供更多信息。这样做的一种方法是删除类型注释,而是使用 const assertion,这有点相反:您明确要求编译器尽可能多地记住有关您分配给的值的特定类型信息变量:

const objArray = [
  { f: (value: string) => value },
  { f: (value: number) => value.toFixed(2) }
] as const;

/* const objArray: readonly [{
  readonly f: (value: string) => string;
 }, {
  readonly f: (value: number) => string;
 }] */

在这里你可以看到编译器推断出 objArray 是正好有两个元素的 readonly tuple,其中第一个元素有一个 string-taking f 方法,并且其中第二个具有 number-taking f 方法。现在您可以毫无错误地执行此操作:

console.log(objArray[0].f('1')); // okay
console.log(objArray[1].f(1)); // okay

万岁。但这在实践中可能不是很有用;据推测,首先将这些对象放入数组是有原因的;如果您想单独跟踪每个对象,您可以首先将它们分配给不同的变量。仍然没有办法获取一个任意 ObjType-like object 并调用它的 f() 方法。


一个函数本身并不能在运行时包含足够的信息来确定它期望的参数类型。这就像一瓶没有标签的药丸。我想您可以尝试使用一些测试输入调用该函数并捕获抛出的任何错误,但这是不好的做法;这就像吃药并“看看会发生什么”。相反,如果 ObjType 类型的值明确标记,您需要选择如何处理它的信息。您想在药瓶上贴上标签。

在 TypeScript 中执行此操作的规范方法是使 ObjType 成为 discriminated union

type StringTaker = {
  takes: "string";
  f: (value: string) => string;
}

type NumberTaker = {
  takes: "number";
  f: (value: number) => string;
}

type ObjType = StringTaker | NumberTaker;

此处,ObjType 类型的对象将具有 takes 属性,其值为 "string""number" 的字符串 literal。通过检查该属性,编译器可以使用结果来了解 f 方法期望的参数类型:

function doSomethingWithObjArray(arr: Array<ObjType>) {
  arr.forEach(o => console.log(o.takes === "string" ? o.f("1") : o.f(1)); // no error
}

当然,这意味着 objArray 需要在每个元素中包含标签:

const objArray: Array<ObjType> = [
  { takes: "string", f: value => value }, 
  { takes: "number", f: value => value.toFixed(2) }
]

(但请注意 value 回调参数是上下文类型的)。现在这无需跟踪数组元素的顺序即可工作:

doSomethingWithObjArray(objArray); // 1, 1.00

Playground link to code