Reputation: 28610
Can we achieve this :
interface IMyInterface{
firstname:string // this means firstname is mandatory
name:string // this also means name is mandatory
}
How can I say, either one of firstname
or name
is optional (?
), depending on if the other one is provided ?
Or if that's not possible, what are the other options ?
EDIT : This is not a duplicate of Typescript Interface - Possible to make "one or the other" properties required?.
We don't want to create a separate interface for every single optional element simply because the maintenance and naming and the refactoring will be a pain in the neck and it's not reusable.
Upvotes: 14
Views: 18035
Reputation: 299
You can write it like this.
interface ContactName{
firstname?: string;
name: string;
}
interface ContactFirstName{
firstname: string
name?: string
}
type Contact = ContactName | ContactFirstName;
And when you use Contact interface, either name or first name become mandatory.
Upvotes: 0
Reputation: 2371
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; })
Upvotes: 0
Reputation: 2564
Another way:
interface IName {
name:string
}
interface IFirstName {
firstname:string
}
let x: IName | IFirstName;
x = {}; // Error
x = { name: "" }; // Ok
x = { firstname: "" }; // Ok
x = { name: "", firstname: "" }; // Ok
Upvotes: 9
Reputation: 3247
Here is a generic way of saying "OneOf" these keys, you might be able to use it here:
type EachOfTmp<T> = {// to make OneOf less gross
[K in Keys<T>]: {
_: {[X in K]: T[K]};
}
};
// require only one of the keys
export type OneOf<T> = EachOfTmp<T>[Keys<T>]["_"] & Partial<T>;
const thing1: OneOf<{ a: number; b: number }> = { a: 2 } // valid
const thing2: OneOf<{ a: number; b: number }> = { b: 2 } // valid
const thing3: OneOf<{ a: number; b: number }> = {} // invalid
EDIT: oops, I forgot I use this convenience Keys deal -
export type Keys<T> = keyof T;
export function Keys<T>(o: T) {
if (!o) {
return [];
}
return Object.keys(o) as Keys<T>[];
}
Upvotes: 12