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'
Generate a permutation of all permitted properties to deal with the unordered nature of dictionary keys.
Playground
EXPLANATION
To simplify things without recursion and conditional types:
{
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"];
}
}
We've structured an object where each value is a tuple with a key.
To accomplish this, every value must contain every key in a different order.
For example:
type AllowedFields = {
countryCode: ["countryCode", 'currency', 'otherFields'];
currency: ["currency", 'countryCode', 'otherFields'];
otherFields: ["otherFields", 'countryCode', 'currency'];
}
Hence, to add two additional props, we need to recursively call TupleUnion
, excluding an element already existing in the tuple. This means our second call should do this:
type AllowedFields = {
countryCode: ["countryCode", Exclude<Fields, 'countryCode'>];
currency: ["currency", Exclude<Fields, 'currency'>];
otherFields: ["otherFields", Exclude<Fields, 'otherFields'>];
}
To achieve this, we use:
TupleUnion<Exclude<U, S>, [...R, S]>;
. It may be clearer if written as:
type TupleUnion<FieldKeys extends string, Result extends any[] = []> = {
[Key in FieldKeys]: TupleUnion<Exclude<FieldKeys, Key>, [...Result, Key]>;
}
If we implemented it like this, we'd end up with deeply nested data structures:
type AllowedFields = TupleUnion<keyof Fields>['countryCode']['currency']['otherFields']
We shouldn't recurse into TupleUnion
if Exclude<U, S>
(or
Exclude<FieldKeys, Key></code) results in <code>never</code. We check if <code>Key
is the last property, and if so, return just
[...R,S]
.
This code snippet:
{
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>
}
is more straightforward. However, we still have an object with values instead of tuples. Each value in the object represents a tuple of the desired type. To get a union of all values, we simply use square bracket notation with a union of all keys, like
type A = {age:1,name:2}['age'|'name'] // 1|2
.
Final adjusted code:
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 square bracket notation with union of all keys