如何从 TypeScript 中的字符串值数组推断类型?

How to infer types from array of string values in TypeScript?

瞄准

  1. 我希望 TS 根据传递给字符串数组的值(静态)推断类型。
  2. 实际的函数实现也应该给出正确的结果

问题

创建一个函数,名为:

export function CreateColoursPalette(tints, palette) {
  //...
}

用法

const tints = {
  sun: createColour({
    10: '#FEF0D6',
    30: '#FCE0AC',
    50: '#FBD183',
    80: '#F8BA44',
    100: '#F7AB1B',
  }),
  azure: createColour<110>({
    10: '#FEF0D6',
    30: '#FCE0AC',
    50: '#FBD183',
    80: '#F8BA44',
    100: '#F7AB1B',
    110: '#FFE0AC',
  })
};


const colours = CreateColoursPalette(tints, {
  primary: ['sun', 'azure'],
  secondary: ['sun'],
  tertiary: {
    neutral: ['sun', 'azure'],
  },
});
  1. tints 是一个包含所有可用色调的对象
  2. palette 是一个包含字符串值数组的对象,这些字符串值是来自 tints 对象的键

该函数应该转换 palette 对象,方法是将数组中的每个字符串键替换为来自 tints 对象的相应值。

智能感知

当我们调用颜色时,TS 应该推断出正确的类型,即

colours.
     //| primary
     //| secondary
     //| tertiary

// here TS should infer only 3 possible options due to the array above
colours.primary.
             //| sun
             //| azure

const [baseColour, tints] = colours.primary.sun;
tints.
   //| 10 | 30 | 50 | 80 | 100

colours.secondary.
               //| sun

colours.tertiary.
              //| neutral

const [baseColour, tints] = colours.tertiary.neutral.azure
tints.
   //| 10 | 30 | 50 | 80 | 100 | 110

我目前的解决方案(TS 输入错误)

为了让这个问题不那么令人生畏,我已将我的解决方案放入 Typescript playground

我正在努力让 TS 根据数组中的值推断类型。

对于打字,您似乎想要这样:

function CreateColoursPalette<T, P extends Palette<keyof T>>(
  tints: T, palette: P): ToColoursPalette<T, P>;

其中 Palette<K>ToColoursPalette<T, P> 的定义如下:

type Palette<K extends PropertyKey> = {
  [k: string]: Array<K> | Palette<K>;
}

type ToColoursPalette<T, P> = {
  [K in keyof P]: (P[K] extends Array<any> ?
    { [L in P[K][number]]: [baseColour: BaseColour, tints: T[L]] } :
    ToColoursPalette<T, P[K]>
  ) };

A Palette<K> 是一个对象,其 属性 值是 K 中的元素数组或另一个 Palette<K> 中的元素数组。这是一个递归定义,应该允许您传入嵌套的调色板。

ToColoursPalette<T, P> 表示所需的调色板类型转换 P。它也是递归的。让我们来看看它:{[K in keyof P]: ...} 意味着它是一个映射类型,其中输出值将具有与 P 相同的键。对于每个这样的键 K,我们检查它是否是一个数组:P[K] extends Array<any> ? 。如果不是,那么我们向下递归,我们希望键 K 处的 属性 值为 ToColoursPalette<T, P[K]>。如果是这样,那么我们想要获取该数组的元素(即 P[K][number],如果您使用数字键索引到 P[K] 则得到的类型)并使用这些元素创建一个新的对象类型作为键。对于 P[K][number] 中的每个这样的键 L,我们将 属性 值设为二元 tuple。第一个元素是 BaseColour,其类型和详细信息不是您要询问的内容。第二个元素是 T[L] 类型,这是当您查找名为 L 的着色对象类型 T 的 属性 时得到的。


JavaScript 中 CreateColoursPalette() 的实现应该类似于:

function CreateColoursPalette(tints, palette) {
  return Object.fromEntries(Object.entries(palette).map(([k, v]) => {
    if (Array.isArray(v)) {
      return [k, Object.fromEntries(v.map(p => [p, [baseColour, tints[p]]]))];
    } else {
      return [k, CreateColoursPalette(tints, v)];
    }
  }));
}

我正在使用 Object.entries() and Object.fromEntries() 将对象转换为数组,反之亦然。

JS 代码做的事情与打字非常相似;我们正在将 palette 转换为具有相同键 k 的新对象,其输出值取决于输入值 v 是否为数组。如果v不是数组,那么我们向下递归调用CreateColoursPalette(tints, v)。否则,我们想要生成一个对象,它的键是 v 的元素,它的值是一个二元组,它的第一个元素是一些我们不关心的 baseColour 东西,它的第二个元素是 tints[p].


这基本上就是全部内容,除了没有很好的方法让编译器查看 JavaScript 代码并验证它是否符合 TypeScript 类型。如果您尝试,您将倾向于 运行 进入编译器错误。要将 JS 实现转换为 TS 接受的东西,最简单的方法是使用 type assertions or the any type 或类似的方法对实现进行 loosen/disable 类型检查。本质上,我们自己负责确保实现和类型兼容。

它可能看起来像这样:

function CreateColoursPalette<T, P extends Palette<keyof T>>(
  tints: T, palette: P): ToColoursPalette<T, P>;
function CreateColoursPalette(tints: any, palette: any): any {
  return Object.fromEntries(Object.entries(palette).map(([k, v]: [string, any]) => {
    if (Array.isArray(v)) {
      return [k, Object.fromEntries(v.map(p => [p, [baseColour, tints[p]]]))];
    } else {
      return [k, CreateColoursPalette(tints, v)];
    }
  }));
}

我正在使用单一呼叫签名 overloaded function 来声明类型,并有意松散 any 实现。没有编译器错误。


我们现在可以验证它是否按预期运行:

console.log(colours.primary.azure[1]["110"]) // "#FFE0AC"
console.log(colours.tertiary.neutral.sun[1]["10"]) // "#FEF0D6"

IntelliSense 一路用正确的键提示,控制台日志输出表明该实现也符合我们的预期。看起来不错!


Playground link to code