当你说“正确”类型时,我猜你的意思是你想要编译器可以推断的最窄的类型。
这是否“适当”在旁观者的眼中;运行时代码 let x = 0 在 TypeScript 中可能有许多可能的类型,编译器只能选择其中一种,除非您自己注释类型。
例如,我可能希望它被解释为let x: number = 0,这是默认的编译器行为,如果我打算稍后为x 分配不同的数字,这很有意义。或者我可能想要let x: 0 | 1 = 0,因为我只会编写像x = y & 1 这样的代码,而且我知道只有那些值会出来。或者我可能想要let x: 0 = 0,因为x 将永远是0(尽管在这种情况下我可能会使用const)。或者也许我想要let x: number | string = 0,因为有时我会给它分配string 值。或者,也许我想要任何其他包含 0 作为值的疯狂类型。
如果你自己不注释,那么编译器必须推断类型,所以它做出一个假设:非readonly 和非const 值将倾向于被推断为“加宽”类型像string、number 或boolean,而readonly 和const 值将倾向于被推断为“窄”literal type 像"foo"、0 或true .
有时编译器的默认假设与开发人员的意图不符。这并不意味着编译器做错了什么。并不是说它需要let x = 0 并推断string 为x。这只是意味着开发人员可能需要付出更多努力来传达他或她的意图。
所以,当我写["foo", "bar"] 时,编译器倾向于将其推断为string[]。如果我想要别的东西,我可以自己注释。让我们从这个版本的函数开始:
function oneItem<T>(arr: readonly T[]): T {
return arr[~~(Math.random() * arr.length)];
}
以下是一些输出:
let arr1 = ["foo", "bar"]; // inferred as string[]
const res1 = oneItem(arr1); // string
let arr2: Array<"foo" | "bar"> = ["foo", "bar"];
const res2 = oneItem(arr2); // "foo" | "bar"
let arr3: ["foo", "bar"] = ["foo", "bar"];
const res3 = oneItem(arr3); // "foo" | "bar"
let arr4: readonly ["foo", "bar"] = ["foo", "bar"];
const res4 = oneItem(arr4); // "foo" | "bar"
let arr5 = ["foo", "bar"] as const; // inferred as readonly ["foo", "bar"];
const res5 = oneItem(arr5);
您可以看到arr1 使用默认的string[] 推断类型,因此string 从函数中出来。对于arr2、arr3 和arr4,我已将数组注释为逐渐变窄的类型,并且它们都允许"foo" | "bar" 退出函数。最后一个arr5 使用const assertion 要求编译器将类型推断为它可以推断的最窄值,对应于arr4。
因此,您可以继续进行的一种方法是更严格地指定值的类型。
当然,在你的用例中,你有类似的东西
const resArrayLiteral = oneItem(["foo", "bar"]); // string
您希望出现"foo" | "bar" 而不是string。为什么oneItem() 的调用者 必须要求编译器缩小数组字面量["foo", "bar"] 的类型? oneItem() 的 implementer 不能使用某种上下文类型提示来告诉编译器更窄地解释事物吗?
嗯,有点,是的。它只是不漂亮。这是一种方法:
type Narrowable = string | number | boolean | symbol | object | undefined | void | null | {};
function oneItem<T extends Narrowable>(arr: readonly T[]): T {
return arr[~~(Math.random() * arr.length)];
}
在这里,我们将 T 和 constraint 分配给 Narrowable,这是一系列事物的联合类型,包括 string、number 和 boolean。事实上,Narrowable 类型是unknown 的一种乏味版本,因为大多数东西都应该分配给它。但是Narrowable 向编译器提供了一个提示(请参阅microsoft/TypeScript#10676),如果可能的话,您希望将T 推断为文字类型。
让我们看看它是否有效:
const resArrayLiteral = oneItem(["foo", "bar"]); // "foo" | "bar"
现在看起来不错! T 被推断为"foo" | "bar" 而不是string。
不过,正如我所说:它并不漂亮。有一个open issue, (microsoft/TypeScript#30680),表明我们可以将oneItem()签名写成类似
function oneI<const T>(arr: readonly T[]): T; // currently invalid syntax
const 在签名中的位置,其行为类似于让调用者使用as const。不过,不确定这个问题是否会发生。现在,我们必须跳过像Narrowable 这样的圈套。
另外,请注意上述T extends Narrowable 不会改变arr1 的行为。如果你写let arr1 = ["foo", "bar"],那么arr将被推断为string[]。一旦它是 string[],编译器就会忘记所有关于 "foo" 和 "bar" 的文字类型。因此oneItem(arr1) 将返回string; T 将被推断为 string。所以在某些时候你可能会发现自己需要自己编写窄类型,如果你想要的话。
好的,希望对您有所帮助;祝你好运!
Playground Link to code