首页 > 解决方案 > 打字稿通用联合解析顺序

问题描述

我正在尝试使用字符串 Union 作为类型参数创建一个接口。它有两个孩子,都与联盟有关。这个想法是它们通过类型参数相互协调。

这是一个简单的复制:

interface IOptions<TValue extends string> {
  values: {[key in TValue]: string};
  items: IItem<TValue>[];
}
interface IItem<TValue extends string> {
  value: TValue;
}

function printOptions<TValue extends string>(options: IOptions<TValue>) {
  console.log(options);
}

printOptions({
  values: {a: 'a', b: 'b'},
  items: [
    { value: 'a'}
  ]
});

https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgJIHkAOZgHsQDOAPACoBqcANgK4oQAekIAJgcgWFKAOYB8yAbwBQyZADcqtAgC5BAbQDWEAJ7JQycpIgBdWRy4huAXwDcItZAC2MtKiukKNCLznazRoaEixEKVHYhLBy1kBiZWdk4efmFRCSdZTSd3ISEYahAEHHxkTAMwLGzCYKdQxggWNn1ogApcbDxCWQwG-GIk2l4ASkFzBDbcSggAOkpcbjrWwi6UoTyvQsaCGtjxLRsBOFkAcjhtgBpkACMdo+2jffNgKxs5c1EBNYTkXfPzbSEjGaA

此代码产生以下错误:

Type '{ a: string; b: string; }' is not assignable to type '{ a: string; }'.

我对发生的事情的猜测是,打字稿决定该items数组将是定义通用 Union 是什么的孩子,然后抱怨values具有未知属性。

我的问题是,我可以以某种方式将打字稿values用作“联合定义器”类型吗?或者以其他方式使其values具有不出现在中的键items

标签: typescriptgenericstypes

解决方案


您只需要推断参数的每个键和值。考虑这个例子:

interface Option<Values, Items> {
  values: Values;
  items: Items;
}

type Item<Value extends PropertyKey> = { value: Value }

function printOptions<
  Value extends string,
  Values extends Record<Value, Value>,
  InferedItem extends Item<keyof Values>,
  Items extends InferedItem[]
>(options: Option<Values, Items>) {
}

printOptions({
  values: { a: 'a', b: 'b' },
  items: [
    { value: 'a' }
  ]
});


printOptions({
  values: { a: 'a', b: 'b' },
  items: [
    { value: 'c' } // error
  ]
});

操场

将其视为类型分解。如果你想推断每个值,你应该解构它。换句话说,从下到上创建结果类型。

  1. 首先,您需要推断出key/value属性values Value extends string

  2. 然后,您需要推断整个values属性:Values extends Record<Value, Value>

  3. 与项目相同的方法。您需要推断一项:InferedItem extends Item<keyof Values>

  4. 然后您可以推断所有项目:Items extends InferedItem[]

有兴趣Type Inference on function arguments可以看看我的文章

如果要禁止values在数组中使用键items,则需要提供验证类型助手。您可以在我的文章/博客中找到更多解释和示例。

interface Option<Values, Items> {
  values: Values;
  items: Items;
}

type Item<Value extends PropertyKey> = { value: Value }

type Check<
  Values extends Record<string, string>,
  Items extends Array<any>,
  Cache extends Array<any> = []
  > =
  Items extends []
  ? Cache
  : Items extends [infer Head, ...infer Tail]
  ? Head extends Item<infer Value>
  ? Value extends keyof Values
  ? Check<Values, Tail, [...Cache, Item<never>]>
  : Check<Values, Tail, [...Cache, Item<Value>]>
  : 1
  : Items;


function printOptions<
  Value extends string,
  ItemValue extends string,
  Values extends Record<Value, Value>,
  InferedItem extends Item<ItemValue>,
  Items extends InferedItem[],
  >(options: Option<Values, Check<Values, [...Items]>>) {
}


printOptions({
  values: { a: 'a', b: 'b' },
  items: [
    { value: 'c' }, // ok
    { value: 'a' }, // error
  ]
});

操场

验证算法:遍历items元组并检查每个value它是否从values. 如果是 - 替换value:charvalue:never,否则不要使用item.

Check返回经过验证items的元组,用作 的第二个参数Options。现在,我们最终得到了一个元组,其中每个无效value都被替换为never. SIncenever是不可表示的,它被 TS 编译器突出显示。


推荐阅读