If IMyInterface
has other members that need to be preserved, I'd like to propose a more generalized version of the @Catalyst response.
type EachExpanded<T> = {
[key in keyof T]: { [subKey in key]: T[key]; }
};
type FixedSubset<T, U> = Pick<T, Exclude<keyof T, U>>;
type AtLeastSubset<T, U> = Pick<T, Extract<keyof T, U>>;
type AtLeaseOne<T, U> = FixedSubset<T, U> & EachExpanded<AtLeastSubset<T, U>>[keyof AtLeastSubset<T, U>];
const example1: AtLeaseOne<{ a: number; b: number; c: string; }, 'a' | 'b'> =
{ a: 3, b: 4, c: '4' } // valid
const example2: AtLeaseOne<{ a: number; b: number; c: string; }, 'a' | 'b'> =
{ a: 1, c: '1' } // valid
const example3: AtLeaseOne<{ a: number; b: number; c: string; }, 'a' | 'b'> =
{ b: 2, c: '2' } // valid
const example4: AtLeaseOne<{ a: number; b: number; c: string; }, 'a' | 'b'> =
{ c: '3' } // invalid
Please keep in mind that this response uses the Exclude
and Extract
keyword introduced in TypeScript version 2.8, which was just released. These are part of the conditional types.
In the code, we assume type T
is the original type or interface and U
is the set of keys, of which at least one must be present.
The way it works is that it creates a type by excluding properties that need to be implemented based on the original type T
. This is defined using Pick<T, Exclude<keyof T, U>>
which contains everything not in U
.
Then, another type is created only containing the elements of which at least one must be present, Pick<T, Extract<keyof T, U>>
.
EachExpanded
stores the type for each one the special sets under the same key. For example, if keys 'a'
and 'b'
are to become conditionally optional for the example above, EachExpanded
creates the following type:
{
a: { a: number; };
b: { b: number; };
}
This will be used in the final type with an intersection operator, so at least one of them is enforced to be present.
Essentially, for the example above, we will end up with the following:
{ c: string; } & ({ a: number; } | { b: number; })