如何键入强制对象值以匹配对象键?

How to type-enforce object value to match object key?

我有实现这样类型的对象:

type TMyObject<T extends string> {
  category: T
}

我需要将它们静态存储在另一个对象中,并确保第二个对象的键与 category 字段的值匹配,如下所示:

const myObject: TMyObject<'foo'> = { category: 'foo' }

const dico = {
  foo: myObject, // good
  bar: myObject, // bad: 'bar' key does not match myObject.category
}

我遇到这种情况,因为我有接口扩展IMyObject并将类别字段固定为精确值,像这样:

type TMyFooObject = IMyObject<'foo'>

我花了两个小时试图为 dico 对象创建一个可以按描述工作的类型,但我就是想不出解决这个问题的方法 ^^

重要说明:category 字段和扩展 TMyObject 的可能类型不是静态的,我们不能在这里使用“简单”联合...

一如既往,非常感谢您花时间阅读并回答这个问题!

只要 IMyObjectstring 类型的 cateogory 这就是运行时检查,而不是编译时检查。

您可以将检查强制执行为每个案例的特定类型的编译时检查:

interface IFooObject {
  category: 'foo';
  foo: number;
  //other foo props
}

interface IBarObject {
  category: 'bar';
  bar: number;
  //other bar props
}

type IObject = IFooObject | IBarObject;

const myObject: IObject = { category: 'foo', foo: 123 }

type Disco<T> = T extends { category: infer U } ? U extends string ? { [P in U]: T } : never : never;

const disco: Disco<IObject> = {
  foo: myObject, // good
  bar: myObject, // this now fails!
};

Playground link

你也可以使用泛型类型,但在那种情况下你需要像这样一直传递它:

type TMyObject<T extends string> = {
  category: T
}

const myObject: TMyObject<'foo'> = { category: 'foo' }

type Disco<T> = T extends { category: infer U } ? U extends string ? { [P in U]: T } : never : never;

const dico: Disco<typeof myObject> = {
  foo: myObject, // good
  bar: myObject, // bad: 'bar' key does not match myObject.category
}

Playground link

我们可以利用 mapped types 来根据对象的键定义对象的值。

type Dico<Keys extends string> = {
    [K in Keys]: TMyObject<K>
}

如果我们事先知道所有类别,那么我们可以创建一个以这些类别为键的地图。

const dico: Dico<'foo' | 'bar'> = {
  foo: { category: 'foo' }, // good
  bar: { category: 'foo' }, // error: Type '"foo"' is not assignable to type '"bar"'
}

如果您想将其限制为一组有效类别但不要求所有类别都存在,则可以使用 Partial<Dico<Category>>

只有当您的类型设置为您已经可以轻松访问所有类别的联合类型时,这才有意义。

我们希望能够只查看对象并查看其键是否与其类别字符串匹配。我们可以做到这一点,但前提是我们通过恒等函数创建对象。泛型函数允许我们从对象推断 Keys 类型。

const makeDico = <Keys extends string>(dico: Dico<Keys>) => dico;

const dico = makeDico({
  foo: { category: 'foo' }, // good
  bar: { category: 'foo' }, // error: Type '"foo"' is not assignable to type '"bar"'
});
// dico has type: Dico<"foo" | "bar">

Typescript Playground Link