【问题标题】:Restrain inferred type of Object.fromEntries限制 Object.fromEntries 的推断类型
【发布时间】:2021-07-19 22:11:06
【问题描述】:

以下函数的推断返回类型为{[k: string]: number}

export const dateParts = (
  date: Date,
  timeZone?: string
) => {
  const options = {
    day: "numeric",
    month: "numeric",
    timeZone,
    year: "numeric",
  } as const;
  const formatter = new Intl.DateTimeFormat("en", options);
  const parts = formatter.formatToParts(date);
  return Object.fromEntries(
    parts
      .filter(({ type }) => ["year", "month", "day"].includes(type))
      .map(({ type, value }) => [type, Number(value)])
  );
};

但对于返回对象的键,唯一可能的选项是 yearmonthday

有没有办法将返回对象的键限制为只有那些?

我尝试将返回类型显式添加为{'year': number, 'month': number, 'day': number}Record<'year' | 'month' | 'day', number>

export const dateParts = (
  date: Date,
  timeZone?: string
): { year: number; month: number; day: number } => {

但它与 Object.fromEntries 返回的内容冲突:

Type '{ [k: string]: number; }' is missing the following properties from type '{ year: number; month: number; day: number; }': 'year', 'month', 'day' ts(2739)

我无法弄清楚如何修改Object.fromEntries 的返回类型,使其匹配{'year': number, 'month': number, 'day': number} 而不是{[k: string]: number}

【问题讨论】:

标签: javascript typescript types typescript-typings


【解决方案1】:

不幸的是,编译器无法在类型级别上严格遵循您的逻辑,从而为您提供所需的类型。在此过程中的每一步:filter()map()Object.fromEntries(),编译器都会生成正确但太宽而无法使用的类型。

最终你会想要像这样使用type assertion

  return Object.fromEntries(
    parts
      .filter(({ type }) => ["year", "month", "day"].includes(type))
      .map(({ type, value }) => [type, Number(value)])
  ) as Record<'year' | 'month' | 'day', number>;

对于像这种情况,这是一种合理的方法,在这种情况下,您比编译器更了解值的类型。


另一种方法是为filter()map()Object.fromEntries() 提供您自己的更具体的类型,它们可能只对这行特定的代码行有用,并且有自己的注意事项。

例如,编译器没有意识到({type} =&gt; ["year", "month", "day"].includes(type)) 在其输入中充当type guard。类型保护函数不会自动推断;您必须自己将其注释为 user-defined type guard function,如下所示:

const filteredParts = parts
  .filter((part): part is { type: "year" | "month" | "day", value: string } =>
    ["year", "month", "day"].includes(part.type))
/* const filteredParts: {
type: "year" | "month" | "day";
value: string;
}[] */

现在filter() 的输出将足够窄。这里需要注意的是,编译器只是相信用户定义的类型保护按照它所说的去做,所以它就像一个类型断言。如果你写了["foo", "bar", "baz"].includes(part.type),编译器就不会注意到或警告你。

然后,当您在map() 中返回一个数组字面量时,例如[key, val],编译器会将其扩展为无序数组类型,例如Array&lt;typeof key | typeof val&gt;,而您特别需要一个tuple type,例如[typeof key, typeof val]。所以你需要改变这种行为,可能通过使用const assertion

const mappedParts = filteredParts.map(
  ({ type, value }) => [type, Number(value)] as const
);
// const mappedParts: (readonly ["year" | "month" | "day", number])[]

太好了,现在你有东西要传递给Object.fromEntries()。不幸的是,TypeScript standard library's type definitions for Object.fromEntries() 只返回 any,或者带有 string index signature 的东西。

如果你愿意,你可以merge in你自己的类型签名,它返回一个更准确地表示事物的key-remapped type(如果你使用模块,它必须是global augmentation),像这样:

interface ObjectConstructor {
  fromEntries<T extends readonly [PropertyKey, any]>(
    entries: Iterable<T>
  ): { [K in T as T[0]]: T[1] };
}

然后,最后,您的返回值将按照您想要的方式进行强类型化:

const ret = Object.fromEntries(mappedParts);
/* const ret: {
  year: number;
  month: number;
  day: number;
} */

这一切都值得吗?我对此表示怀疑。不过这取决于你。

Playground link to code

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-07-17
    • 2013-04-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多