首页 > 解决方案 > 如何使它成为可扩展的打字稿类型?

问题描述

我有一个MeasurementData看起来像这样的类型。

type MeasurementData = {
  measurements: {
    x: number | Date;
    y: number;
  }[];

  xUnits: string;
  xFirstValue: number;
  xLastValue: number;
  xSecondToLastValue: number;
  xAverage: number;
  xMin: number;
  xMax: number;
  xDelta: number;

  yUnits: string;
  yFirstValue: number;
  yLastValue: number;
  ySecondToLastValue: number;
  yAverage: number;
  yMin: number;
  yMax: number;
  yDelta: number;
};

但是现在该measurements部分将扩展为可能未知数量的y值。
例如

measurements: {
  x: number | Date;
  y0: number;
  y1: number;
  y2: number;
  y3: number;
  y4: number;
  y5: number;
}[];

作为打字稿的新手,我不确定如何扩展MeasurementData以处理这种情况。

你是如何做到的,这样你就不会重复所有的 y 计算定义?
例如 - 我不想这样做。

y0Units: string;
y0FirstValue: number;
y0LastValue: number;
y0SecondToLastValue: number;
y0Average: number;
y0Min: number;
y0Max: number;
y0Delta: number;

y1Units: string;
y1FirstValue: number;
y1LastValue: number;
y1SecondToLastValue: number;
y1Average: number;
y1Min: number;
y1Max: number;
y1Delta: number;

y2Units: string;
y2FirstValue: number;
y2LastValue: number;
y2SecondToLastValue: number;
y2Average: number;
y2Min: number;
y2Max: number;
y2Delta: number;

/// y3, y4, y5 ...etc...

标签: typescript

解决方案


呃,这是一种噩梦类型。这几乎是可能的,但我请求你重构你的数据类型以使用数组,而不是越来越长的属性键列表,键名中包含数字。但我喜欢让 TypeScript 做一些糟糕的事情,所以这里是:

您希望成为不同数量属性的结构的所有版本MeasurementData的无限联合。y喜欢MeasurementDataElement<0> | MeasurementDataElement<0 | 1> | MeasurementDataElement<0 | 1 | 2> | MeasurementDataElement<0 | 1 | 2 | 3> | ...

你不能真正做无限联合。您可以将其作为通用约束,也可以作为最多某个有限数的联合。我将做后者并选择,说,40所以你可以支持y0通过y39

因为这涉及字符串连接和递归,所以我将使用 TypeScript 4.1 beta。TypeScript 4.1 将支持模板文字类型和映射类型键扩充以及递归条件类型

type CommonSuffixProps = {
  Units: string;
  FirstValue: number;
  LastValue: number;
  SecondToLastValue: number;
  Min: number;
  Max: number;
  Delta: number;
}

type CommonPrefixes<N extends number> = 'x' | `y${N}`

type MeasurementDataElement<N extends number> = Pick<
  { measurements: { [K in CommonPrefixes<N>]: ('x' extends K ? Date : never) | number }[]; } &
  { [K in keyof CommonSuffixProps as `${CommonPrefixes<N>}${K}`]: CommonSuffixProps[K] },
  'measurements' | `x${keyof CommonSuffixProps}` | (N extends any ? `y${N}${keyof CommonSuffixProps}` : never)
> extends infer O ? { [K in keyof O]: O[K] } : never;

type TuplesOfUnionsOfNumbers<N extends number, A extends number[] = []> =
  N extends A['length'] ? A : TuplesOfUnionsOfNumbers<N, [...A, A[number] | A['length']]>;

type MeasurementDataForTuplesOfUnionsOfNumbers<VN extends { [k: number]: number }> = {
  [K in keyof VN]: MeasurementDataElement<Extract<VN[K], number>>
}[number]

// only works up to maximum number of 40 (y0, y1, y2, y3, y4, ... y38, y39)
type MeasurementData = MeasurementDataForTuplesOfUnionsOfNumbers<TuplesOfUnionsOfNumbers<40>>

那是一团糟 ,我不确定是否值得详细解释。我将重点介绍:

  • 您可能会看到这CommonSuffixProps看起来像是您想要添加到 and 等末尾xy0内容y1

  • CommonPrefixes<N>类型0 | 1 | 2变成_'x' | 'y0' | 'y1' | 'y2'

  • MeasurementDataElement<N>类型变成0 | 1你正在寻找的类型,它的属性measurements{x: number | Date, y0: number, y1: number}[]哪里,它的其他属性看起来都像结合CommonPrefixes<0 | 1>CommonSuffixProps.

  • TuplesOfUnionsOfNumbers<N>类型变成4tuple 。[0, 0|1, 0|1|2, 0|1|2|3]我正在使用它,所以我们可以将单个数字转换为我们想要的可能的属性集集40的大列表。y

  • 获取之前的MeasurementDataForTuplesOfUnionsOfNumbers<T>那些 tuples-of-unions-of-numbers 之一,并为您提供其中MeasurementDataElement<N>所有的N并集。

  • 最后,MeasurementDatais just MeasurementDataForTuplesOfUnionsOfNumbers<TuplesOfUnionsOfNumbers<40>>,或者我们正在寻找的类型。


使用 IntelliSense 检查这种类型非常烦人。假设我们MeasurementData查看具有y1Min属性但没有y2Min属性的单个联合元素:

type Example = Extract<Exclude<MeasurementData, {y2Min: any}>, {y1Min: any}>;
/* type Example = {
    measurements: {
        x: number | Date;
        y0: number;
        y1: number;
    }[];
    xUnits: string;
    xFirstValue: number;
    xLastValue: number;
    xSecondToLastValue: number;
    xMin: number;
    xMax: number;
    xDelta: number;
    y0Units: string;
    y0FirstValue: number;
    y0LastValue: number;
    y0SecondToLastValue: number;
    ... 9 more ...;
    y1Delta: number;
} */

我猜这看起来对吗?作为进一步的证据,让我们尝试对 type 的值进行一些测试MeasurementData

declare const md: MeasurementData;
if ("y23Min" in md) {
  md.y14SecondToLastValue.toFixed(2);
  for (let m of md.measurements) {
    m.y19.toFixed(2);
  }
}

通过测试 if y23Minis in the value,编译器知道它是至少有 24 个y坐标的联合元素之一,所以它会让你索引到,比如说,y14SecondToLastValuemeasurements[0].y19

万岁!


你去吧。再次,我恳请您不要使用这种类型。相反,我强烈建议您重构为对 TypeScript 更友好的数据结构。也许是这样的?

interface Friendly<Y extends CommonSuffixProps[]> {
  measurements: { x: number, y: { [K in keyof Y]: number } }[],
  axisProperties: { x: CommonSuffixProps, y: Y }
}
function acceptFriendly<Y extends CommonSuffixProps[]>(f: Friendly<[...Y]>) { }

declare const axisProps: CommonSuffixProps;
acceptFriendly({
  measurements: [
    { x: 0, y: [1, 2, 3] },
    { x: 1, y: [2, 3, 4] }
  ],
  axisProperties: {
    x: axisProps,
    y: [axisProps, axisProps, axisProps]
  }
})

这是属性数量的通用约束y,否则我们将更y3Min改为类似axisProperties.y[3].Min. 这也可以更容易地迭代,对吧?但最终是否要重构取决于您。即使你不想,我也不能真正推荐上面那种类型的科学怪人。请认为这是一个警示故事,而不是解决方案。


Playground 代码链接


推荐阅读