Typescript 函数,它从具有正确类型的数组中返回一项

Posted

技术标签:

【中文标题】Typescript 函数,它从具有正确类型的数组中返回一项【英文标题】:Typescript function which returns one item from an array with proper type 【发布时间】:2020-04-19 09:51:54 【问题描述】:

我有一个从数组中返回一项的函数:

function oneItem(arr: any[]) 
  return arr[~~(Math.random() * arr.length)];

我想为此提供合适的类型。


我的尝试:

type ValuesOf<T extends any[]>= T[number];
function oneItem<ARRAY extends any[]>(arr: ARRAY): ValuesOf<ARRAY>  ... 

const a = oneItem(["foo", "bar"]);
// typeof a is (string) instead of ("foo" | "bar")

在这里找到 (https://github.com/Microsoft/TypeScript/issues/20965)


如何从具有正确类型的数组中返回单个项目?


编辑:我想出了只读数组:

type ValuesOf<T extends readonly any[]>= T[number];
function oneItem<ARRAY extends readonly any[]>(arr: ARRAY): ValuesOf<ARRAY>  ... 

const a = oneItem(["foo", "bar"] as const);

【问题讨论】:

我错过了一些东西——为什么function oneItem&lt;T&gt;(arr: T[]): T不够好? const b = oneItem(["foo", "bar"]); ---> typeof b (string) 它应该是什么? 啊,我想我明白了!需要学习如何阅读:) 【参考方案1】:

当你说“正确”类型时,我猜你的意思是你想要编译器可以推断的最窄的类型。

这是否“适当”在旁观者的眼中;运行时代码 let x = 0 在 TypeScript 中可能有许多可能的类型,编译器只能选择其中一种,除非您自己注释类型。

例如,我可能希望它被解释为let x: number = 0,这是默认的编译器行为,如果我打算稍后为x 分配不同的数字,这很有意义。或者我可能想要let x: 0 | 1 = 0,因为我只会编写像x = y &amp; 1 这样的代码,而且我知道只有那些值会出来。或者我可能想要let x: 0 = 0,因为x 将永远是0(尽管在这种情况下我可能会使用const)。或者也许我想要let x: number | string = 0,因为有时我会给它分配string 值。或者,也许我想要任何其他包含 0 作为值的疯狂类型。

如果你自己不注释,那么编译器必须推断类型,所以它做出一个假设:非readonly 和非const 值将倾向于被推断为“加宽”类型像stringnumberboolean,而readonlyconst 值将倾向于被推断为“窄”literal type 像"foo"、0true .

有时编译器的默认假设与开发人员的意图不符。这并不意味着编译器做错了什么。并不是说它需要let x = 0 并推断stringx。这只是意味着开发人员可能需要付出更多努力来传达他或她的意图。


所以,当我写["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 从函数中出来。对于arr2arr3arr4,我已将数组注释为逐渐变窄的类型,并且它们都允许"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,这是一系列事物的联合类型,包括 stringnumberboolean。事实上,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) 将返回stringT 将被推断为 string。所以在某些时候你可能会发现自己需要自己编写窄类型,如果你想要的话。


好的,希望对您有所帮助;祝你好运!

Playground Link to code

【讨论】:

以上是关于Typescript 函数,它从具有正确类型的数组中返回一项的主要内容,如果未能解决你的问题,请参考以下文章

具有多种类型的函数声明

在 TypeScript 中使用 Promises 的异步函数的正确错误模式

映射具有未知深度的通用 TypeScript 接口

Typescript 类型检查具有混合类型和混合泛型的数组

TypeScript类型断言数组的类型函数的类型

Typescript - 具有通用类型函数的索引签名