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'
You need to create a permutation of all allowed props. Why permutation ? Because keys of dictionary are unordered.
Playground
EXPLANATION
Let's get rid of recursive call and conditional type:
{
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 have created an object, where each value is a tuple with key
.
In order to get things done, each value, should contain each key in different order.
Smth like that:
type AllowedFields = {
countryCode: ["countryCode", 'currency', 'otherFields'];
currency: ["currency", 'countryCode', 'otherFields'];
otherFields: ["otherFields", 'countryCode', 'currency'];
}
Hence, in order to add two other props, we need to call TupleUnion
recursively, but without an element which already exists in a tuple. It means, that 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, it we need do this: TupleUnion<Exclude<U, S>, [...R, S]>;
. Maybe it will be much readable if I write:
type TupleUnion<FieldKeys extends string, Result extends any[] = []> = {
[Key in FieldKeys]: TupleUnion<Exclude<FieldKeys, Key>, [...Result, Key]>;
}
But then we will gent deep nested data structure:
type AllowedFields = TupleUnion<keyof Fields>['countryCode']['currency']['otherFields']
We should not call TupleUnion
recursion if Exclude<U, S>
, or in other words Exclude<FieldKeys, Key>
returns never
. We need to check if Key
is a last property. In order to do that, we can check if Exclude<U, S> extends never
. IF it is never
- no more keys, we can just return [...R,S]
.
I hope that this 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]>;
}
type AllowedFields = TupleUnion<keyof Fields>
}
is much clearer. However, we still have an object with values instead of tuple. Each value in object is a tuple of desired type. In order to get a union of all values, we just need to use square bracket notation with union of all keys. Smth like that: type A = {age:1,name:2}['age'|'name'] // 1|2
.
Final 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 suqare bracket notation with union of all keys