typescript 我如何使这个泛型类类型安全?

1cklez4t  于 2023-05-19  发布在  TypeScript
关注(0)|答案(1)|浏览(113)

我有一个typescript函数register(values, f),其中值的类型为{ key1: string, key2: string }[],f是一个接受变量context的函数。context是一个泛型类,它提供了一个函数getValueForKeys,其输入是key1key2,它们对应于通过values参数传入寄存器的{ key1: string, key2: string }对象之一。如何使这个泛型类类型安全,使得从f的函数定义中的上下文访问的所有键都是传递到register()的键?我希望从register()的用法中推断上下文,这样编写要注册的函数的开发人员就不需要向泛型传递类型。
我试着做了如下的解决方案:

type Key = { key1: string; key2: string; value: any };
type Values = Key[];

type Key2FromKey1<T extends Values, K1 extends T[number]['key1']> = T[number]['key1'] extends K1 ? T[number]['key2'] : never;

class Context<T extends Values> {
  constructor(private keys: T) {}

  getValueForKey<K1 extends T[number]['key1'], K2 extends Key2FromKey1<T, K1>>(key1: K1, key2: K2): any {
    const matchingKey = this.keys.find(k => k.key1 === key1 && k.key2 === key2);
    return matchingKey ? matchingKey.value : undefined;
  }
}

function register<T extends Values>(
  values: T,
  f: (context: Context<T>) => void
) {
  const context = new Context(values);
  f(context);
}

register([{ key1: 'foo', key2: 'bar', value: 'value1' }, { key1: 'biz', key2: 'baz', value: 'value2' }], (context) => {
  // This should succeed
  const value = context.getValueForKey('foo', 'bar');
  // This should fail
  const value2 = context.getValueForKey('biz', 'buzz'); // Error: Argument of type '"buzz"' is not assignable to parameter of type '"baz"'.
});

然而,这似乎对value和value2都失败了,对于'bar'和'buzz',Argument of type 'string' is not assignable to parameter of type 'never'

lmvvr0a8

lmvvr0a81#

主要的问题是您对Key2FromKey1<T, K1>的定义实际上并没有提取输入数组类型的正确元素。由于T[number]Key-ish类型的联合,因此您可以使用Extract实用程序类型来过滤key1可分配给K1的联合,然后index into"key2"属性的成员:

type Key2FromKey1<T extends Values, K1 extends T[number]['key1']> =
  Extract<T[number], { key1: K1 }>['key2'];

另一个问题是,默认情况下,编译器只会推断出字符串值属性的string;因此,值{key1: "foo"}将被解释为{key1: string},而您关心的文字类型"foo"将丢失。您需要给予编译器一个提示,即register()的第一个参数需要更严格地推断其类型,就像调用者使用constAssert一样。我们可以通过为T使用const类型参数来做到这一点:

function register<const T extends Values>(
  // -----------> ^^^^^
  values: T,
  f: (context: Context<T>) => void
) {
  const context = new Context(values);
  f(context);
}

哦,这意味着数组文字将被推断为readonly元组类型。这很好,但是ValuesKey[],并且会拒绝readonly Key[]。所以我们再做一个修改来允许它:

type Values = readonly Key[];
// ---------> ^^^^^^^^

好了,这些就是变化。让我们测试一下:

register(
  [
    { key1: 'foo', key2: 'bar', value: 'value1' },
    { key1: 'biz', key2: 'baz', value: 'value2' }
  ], (context) => {
    const value = context.getValueForKey('foo', 'bar'); // okay
    const value2 = context.getValueForKey('biz', 'buzz'); // error!
    // ----------------------------------------> ~~~~~~
    // Argument of type '"buzz"' is not assignable to parameter of type '"baz"'.
  });

T被推断为

readonly [{
    readonly key1: "foo";
    readonly key2: "bar";
    readonly value: "value1";
}, {
    readonly key1: "biz";
    readonly key2: "baz";
    readonly value: "value2";
}]

因此,根据需要,对getValueForKey("foo", ⋯)的调用将第二个参数约束为"bar",而对getValueForKey("biz", ⋯)的调用将第二个参数约束为"baz"
Playground链接到代码

相关问题