Reputation: 43
I've seen this code snippet a lot:
type RequireOnlyOne<T, Keys extends keyof T = keyof T> =
Pick<T, Exclude<keyof T, Keys>>
& {
[K in Keys]-?:
Required<Pick<T, K>>
& Partial<Record<Exclude<Keys, K>, undefined>>
}[Keys]
Here you can find the question from where I took it.
It works, however, I have this structure:
export interface MenuItems {
firstLevel: {
secondLevel: ['one', 'two']
};
anotherFirstLevel: {
anotherSecondLevel: ['one', 'two'],
oneMoreSecondLevel: null
};
}
I need to apply RequireOnlyOne
for the first and the second levels, but I can't figure what to change on the type RequireOnlyOne
so it works for each firstLevel keys but also with secondLevel keys. As it is right now I can select only one firstLevel but multiple secondLevels of that firstLevel.
I also tried to compose a new type with an object which key could be RequireOnlyOne<keyof MenuItems>
and a value that uses also RequireOnlyOne
for the values, but couldn't make it.
Example of what I want, calling the desired type as customType
:
const workingObject: customType = {
firstLevel: { // Just one property of the first level
secondLevel: ['one', 'two'] // Just one property of the second level
};
}
const errorObject: customType = {
firstLevel: {
secondLevel: ['one', 'two']
};
anotherFirstLevel: { // Should not work as I am including 2 properties for the first level
anotherSecondLevel: ['one', 'two']
};
}
const anotherErrorObject: customType = {
anotherFirstLevel: {
anotherSecondLevel: ['one', 'two'],
oneMoreSecondLevel: null // Should not work neither as I am including 2 properties for second level
};
}
The type should throw an error if the object has more than one first level property, and/or more than one second level property. With the proposed RequireOnlyOne
type I can achieve that but just for the first level, but I need the same effect for first and second level.
Any ideas?
Upvotes: 2
Views: 404
Reputation: 4128
IMO it's a quite complex question and I'd recommend looking for alternatives and refactor the code.
Otherwise, you might find this helpful:
type RequireOnlyOne<T, Keys extends keyof T = keyof T> = Pick<T, Exclude<keyof T, Keys>> &
{
[K in Keys]-?: Required<Pick<T, K>> & Partial<Record<Exclude<Keys, K>, undefined>>;
}[Keys];
type RequireOnlyOneUnion<T, Keys extends KeysOfUnion<T> = KeysOfUnion<T>> = Pick<T, Exclude<keyof T, Keys>> &
{
[K in Keys]-?: Required<Pick<T, K>> & Partial<Record<Exclude<Keys, K>, undefined>>;
}[Keys];
export interface MenuItems {
firstLevel: {
secondLevel: ["one", "two"];
};
anotherFirstLevel: {
anotherSecondLevel: ["one", "two"];
oneMoreSecondLevel: null;
};
}
type KeysOfUnion<T> = T extends T ? keyof T : never;
const x: RequireOnlyOne<MenuItems, keyof MenuItems> = { firstLevel: { secondLevel: ["one", "two"] } };
// ok
const a: RequireOnlyOneUnion<MenuItems[keyof typeof x], KeysOfUnion<MenuItems[keyof typeof x]>> = {
anotherSecondLevel: ["one", "two"],
};
// error
const b: RequireOnlyOneUnion<MenuItems[keyof typeof x], KeysOfUnion<MenuItems[keyof typeof x]>> = {
secondLevel: ["one", "two"],
anotherSecondLevel: ["one", "two"],
};
Upvotes: 0
Reputation: 33041
I don't know how to change RequireOnlyOne
type but I know how to create new type.
export interface MenuItems {
firstLevel: {
secondLevel: ['one', 'two']
};
anotherFirstLevel: {
anotherSecondLevel: ['one', 'two']
oneMoreSecondLevel: null
};
}
type Primitives = string | number | boolean | null | undefined | bigint | symbol
type UnionKeys<T> = T extends T ? keyof T : never;
// credits goes to https://stackoverflow.com/questions/65805600/type-union-not-checking-for-excess-properties#answer-65805753
type StrictUnionHelper<T, TAll> =
T extends any
? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;
type StrictUnion<T> = StrictUnionHelper<T, T>
type Transform<Obj, Keys extends keyof Obj = keyof Obj, Result = never> =
StrictUnion<
Keys extends string ? { // #1
[Key in Keys]:
Key extends keyof Obj
? (Obj[Key] extends Primitives
? Obj[Key]
: (Obj[Key] extends any[]
? Obj[Key]
: Transform<Obj[Key], keyof Obj[Key], Obj[Key]>)
)
: never
} : Result>
type CustomType = Transform<MenuItems>
const workingObject: CustomType = {
firstLevel: { // Just one property of the first level
secondLevel: ['one', 'two'] // Just one property of the second level
},
}
const errorObject: CustomType = {
firstLevel: {
secondLevel: ['one', 'two']
},
anotherFirstLevel: { // Should not work as I am including 2 properties for the first level
anotherSecondLevel: ['one', 'two']
},
}
const anotherErrorObject: CustomType= {
anotherFirstLevel: {
anotherSecondLevel: ['one', 'two'],
oneMoreSecondLevel: null // Should not work neither as I am including 2 properties for second level
},
}
Transform
- is a main utility type. Recursively iterates over keys. 1# Keys extends string
- this line makes sure that that Keys is distributet
. It means that whole code which goes after this line will be applied to each key. Please see docs for more info.
I have also added Obj[Key] extends any[]
- because you don't want (I suppose) to iterate though arrays keys.
Upvotes: 1