从对象类型创建常量数组类型



我有一个类似的对象类型:

type Fields = {
countryCode: string;
currency: string;
otherFields: string;
};

我还有一个类似的只读数组:

// Type: readonly ["countryCode", "currency", "otherFields"]
const allowedFields = ["countryCode", "currency", "otherFields"] as const;

我希望能够为这个基于Fields对象类型的数组声明指定一个接口,以便对它的任何更改都需要更改数组。像这样:

// How to create 'SomeType'?
const allowedFields: SomeType = ["countryCode"] as const; // Should throw error because there are missing fields
const allowedFields: SomeType = ["extraField"] as const; // Should throw error because "extraField" is not in the object type 'Fields'
type Fields = {
countryCode: string;
currency: string;
otherFields: string;
};
// credits goes to https://twitter.com/WrocTypeScript/status/1306296710407352321
type TupleUnion<U extends string, R extends any[] = []> = {
[S in U]: Exclude<U, S> extends never ? [...R, S] : TupleUnion<Exclude<U, S>, [...R, S]>;
}[U];

type AllowedFields = TupleUnion<keyof Fields>;

const allowedFields: AllowedFields = ["countryCode", "currency", "otherFields"];

// How to create 'SomeType'?
const foo: AllowedFields  = ["countryCode"]; // Should throw error because there are missing fields
const bar: AllowedFields  = ["extraField"]; // Should throw error because "extraField" is not in the object type 'Fields'

您需要创建所有允许的道具的排列。为什么是排列?因为字典的键是无序的

游乐场

让我们去掉递归调用和条件类型:

{
type TupleUnion<U extends string, R extends any[] = []> = {
[S in U]: [...R, S]
}
type AllowedFields = TupleUnion<keyof Fields>;
type AllowedFields = {
countryCode: ["countryCode"];
currency: ["currency"];
otherFields: ["otherFields"];
}
}

我们已经创建了一个对象,其中每个值都是具有key的元组。为了完成任务,每个值都应该以不同的顺序包含每个键。像那样:

type AllowedFields = {
countryCode: ["countryCode", 'currency', 'otherFields'];
currency: ["currency", 'countryCode', 'otherFields'];
otherFields: ["otherFields", 'countryCode', 'currency'];
}
因此,为了添加另外两个道具,我们需要递归地调用TupleUnion,但是不需要一个已经存在于元组中的元素。这意味着,我们的第二个调用应该这样做:

type AllowedFields = {
countryCode: ["countryCode", Exclude<Fields, 'countryCode'>];
currency: ["currency", Exclude<Fields, 'currency'>];
otherFields: ["otherFields", Exclude<Fields, 'otherFields'>];
}

要实现,我们需要这样做:TupleUnion<Exclude<U, S>, [...R, S]>;。如果我写:

可能会更容易读
type TupleUnion<FieldKeys extends string, Result extends any[] = []> = {
[Key in FieldKeys]: TupleUnion<Exclude<FieldKeys, Key>, [...Result, Key]>;
}

但是接下来我们将使用深度嵌套数据结构:

type AllowedFields = TupleUnion<keyof Fields>['countryCode']['currency']['otherFields']

如果Exclude<U, S>,或者换句话说,Exclude<FieldKeys, Key>返回never,则不应该调用TupleUnion递归。我们需要检查Key是否是最后一个属性。为了做到这一点,我们可以检查Exclude<U, S> extends never。如果它是never-没有更多的键,我们可以只返回[...R,S]

我希望这段代码:

{
type TupleUnion<FieldKeys extends string, Result extends any[] = []> = {
[Key in FieldKeys]: Exclude<FieldKeys, Key> extends never ? [...Result, Key] : TupleUnion<Exclude<FieldKeys, Key>, [...Result, Key]>;
}
type AllowedFields = TupleUnion<keyof Fields>
}

更清晰。但是,我们仍然有一个带值的对象,而不是tuple。object中的每个值都是所需类型的元组。为了得到所有值的并集,我们只需要使用所有键的并集的方括号符号。如:type A = {age:1,name:2}['age'|'name'] // 1|2.

最终代码:

type TupleUnion<FieldKeys extends string, Result extends any[] = []> = {
[Key in FieldKeys]: Exclude<FieldKeys, Key> extends never ? [...Result, Key] : TupleUnion<Exclude<FieldKeys, Key>, [...Result, Key]>;
}[FieldKeys] // added suqare bracket notation with union of all keys

这在概念上与您所要求的相反,因为它允许我们使用数组作为类型来创建对象,而不是根据给定的对象类型对数组设置限制。但是这种相互依赖是双向的,所以你也可以利用它。

从const数组创建类型,然后使用该类型作为键类型创建映射类型。如果忘记在对象上设置const数组值,或者设置数组中不存在的const数组值,就会得到错误。

const categoryNames = ['a', 'b', 'c', 'd'] as const
export type Keys = typeof categoryNames[number]
export type Categories<Key extends string, Type> = {
[name in Key]: Type
}
const mapping: Categories<Keys, number> = {
a: 0,
// b: 1, with 'b' commented out there will be an error
c: 2,
d: 3
}

使用来自https://stackoverflow.com/a/70061272的代码应该避免@captain-yossarian answer(示例)中的Type instantiation is excessively deep and possibly infinite (ts2589):

type Fields = {
countryCode: string;
currency: string;
otherFields: string;
};
type UnionToParm<U> = U extends any ? (k: U) => void : never;
type UnionToSect<U> = UnionToParm<U> extends (k: infer I) => void ? I : never;
type ExtractParm<F> = F extends { (a: infer A): void } ? A : never;
type SpliceOne<Union> = Exclude<Union, ExtractOne<Union>>;
type ExtractOne<Union> = ExtractParm<UnionToSect<UnionToParm<Union>>>;
type ToTuple<Union> = ToTupleRec<Union, []>;
type ToTupleRec<Union, Rslt extends any[]> = SpliceOne<Union> extends never
? [ExtractOne<Union>, ...Rslt]
: ToTupleRec<SpliceOne<Union>, [ExtractOne<Union>, ...Rslt]>;
type AllowedFields = ToTuple<keyof Fields>;

const allowedFields: AllowedFields = ["countryCode", "currency", "otherFields"];

// How to create 'SomeType'?
const foo: AllowedFields  = ["countryCode"]; // Should throw error because there are missing fields
const bar: AllowedFields  = ["extraField"]; // Should throw error because "extraField" is not in the object type 'Fields'

游乐场

最新更新