Reputation: 58632
I'm trying to create an interface that could have
export interface MenuItem {
title: string;
component?: any;
click?: any;
icon: string;
}
component
or click
to be setUpvotes: 280
Views: 212232
Reputation: 892
I like using Pick
along with a base type that includes all properties to establish these kinds of conditional requirements.
interface MenuItemProps {
title: string;
component: any;
click: any;
icon: string;
}
export type MenuItem =
Pick<MenuItemProps, "title" | "icon" | "component"> |
Pick<MenuItemProps, "title" | "icon" | "click">
This is clean and also flexible. You can get arbitrarily complex with your requirements, asserting things like "require either all the properties, just these two properties, or just this one property" and so on while keeping your declaration simple and readable.
Upvotes: 7
Reputation: 584
I use this:
type RequireField<T, K extends keyof T> = T & Required<Pick<T, K>>
Usage:
let a : RequireField<TypeA, "fieldA" | "fieldB">;
This makes fieldA
and fieldB
required.
Upvotes: 18
Reputation: 3605
Here's a simple way to implement either but not both
type MenuItem = {
title: string;
component: any;
click?: never;
icon: string;
} | {
title: string;
component?: never;
click: any;
icon: string;
}
// good
const menuItemWithComponent: MenuItem = {
title: 'title',
component: "my component",
icon: "icon"
}
// good
const menuItemWithClick: MenuItem = {
title: 'title',
click: "my click",
icon: "icon"
}
// compile error
const menuItemWithBoth: MenuItem = {
title: 'title',
click: "my click",
component: "my click",
icon: "icon"
}
Upvotes: 6
Reputation: 74800
There is a simpler solution. No need to rely on any
or
complex conditional types (see answer):
- Is there a way to require component or click to be set? (Inclusive
OR
)
type MenuItemOr = {
title: string;
icon: string;
} & ({ component: object } | { click: boolean })
// brackets are important here: "&" has precedence over "|"
let testOr: MenuItemOr;
testOr = { title: "t", icon: "i" } // error, none are set
testOr = { title: "t", icon: "i", component: {} } // ✔
testOr = { title: "t", icon: "i", click: true } // ✔
testOr = { title: "t", icon: "i", click: true, component: {} } // ✔
A union type (|
) corresponds to inclusive OR
. It is intersected with the non-conditional properties.
Use the in
operator to narrow the value back to one of the constituents:
if ("click" in testOr) testOr.click // works
- Is there a way to require that both properties can't be set? (Exclusive
OR
/XOR
)
type MenuItemXor = {
title: string;
icon: string;
} & (
| { component: object; click?: never }
| { component?: never; click: boolean }
)
let testXor: MenuItemXor;
testXor = { title: "t", icon: "i" } // error, none are set
testXor = { title: "t", icon: "i", component: {} } // ✔
testXor = { title: "t", icon: "i", click: true } // ✔
testXor = { title: "t", icon: "i", click: true, component: {} } //error,both set
Basically either component
or click
can be set, the other should never 1 be added at the same time. TS can make a discriminated union type out of MenuItemXor
, which corresponds to XOR
.
This XOR
condition for MenuItemXor
is not possible with accepted answer.
1 Technically, prop?: never
gets resolved to prop?: undefined
, though former is often used for illustration.
Upvotes: 147
Reputation: 169
This approach combines never
and Omit
. Benefits here are that it's easy to understand and also easy to update if you need to add more properties.
interface Base {
title: string;
icon: string;
component?: never;
click?: never;
}
interface OnlyComponent {
component: any;
}
interface OnlyClick {
click: any;
}
export type MenuItem = (Omit<Base, 'component'> & OnlyComponent) | (Omit<Base, 'click'> & OnlyClick);
You can use in
to narrow an instance of MenuItem
:
const item: MenuItem = {
title: 'A good title';
icon: 'fa-plus';
component: SomeComponent;
};
//...
if('component' in item) {
const Comp = item.component;
//...
}
Upvotes: 1
Reputation: 51
Yet another solution:
type RequiredKeys<T, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>;
type MenuItem2 = RequiredKeys<MenuItem, "component" | "click">;
Upvotes: 3
Reputation: 20980
To just extends upon the cool answers above! And for the people that land here while searching for a Partial version with requiring capability! Here a snippet i made to take!
You want to have a Partial of an interface, but in mean time require some of the fields! Here how it's done
export type PartialReq<T, Keys extends keyof T = keyof T> =
Pick<Partial<T>, Exclude<keyof T, Keys>>
& {
[K in Keys]: T[K]
};
export interface CacheObj<SigType = any, ValType = any> {
cache: Map<SigType, ValType>,
insertionCallback: InsertionCallback<SigType, ValType> // I want this to be required
}
// ...
export class OneFlexibleCache<SigType = any, ValType = any> {
private _cacheObj: CacheObj<SigType, ValType>;
constructor(
cacheObj: PartialReq<CacheObj<SigType, ValType>, 'insertionCallback'> // <-- here
// i used it
) {
cacheObj = cacheObj || {};
this._cacheObj = {
// ...
// _______________ usage
this._caches.set(
cacheSignature,
new OneFlexibleCache<InsertionSigType, InsertionValType>({
insertionCallback // required need to be provided
})
);
Here you can see that it work perfectly
If the required not provided
I just went by the doc and found Omit.
https://www.typescriptlang.org/docs/handbook/utility-types.html#omittk
I came to add it. But before i do, I just seen this cool answer. It cover all:
https://stackoverflow.com/a/48216010/7668448
Just check it out! It show how to do it for all the different version of Typescript! And for the sake of not repeating ! Go and check!
Upvotes: 0
Reputation: 5893
With the help of the Exclude
type which was added in TypeScript 2.8, a generalizable way to require at least one of a set of properties is provided is:
type RequireAtLeastOne<T, Keys extends keyof T = keyof T> =
Pick<T, Exclude<keyof T, Keys>>
& {
[K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>
}[Keys]
And a partial but not absolute way to require that one and only one is provided is:
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 is a TypeScript playground link showing both in action.
The caveat with RequireOnlyOne
is that TypeScript doesn't always know at compile time every property that will exist at runtime. So obviously RequireOnlyOne
can't do anything to prevent extra properties it doesn't know about. I provided an example of how RequireOnlyOne
can miss things at the end of the playground link.
A quick overview of how it works using the following example:
interface MenuItem {
title: string;
component?: number;
click?: number;
icon: string;
}
type ClickOrComponent = RequireAtLeastOne<MenuItem, 'click' | 'component'>
Pick<T, Exclude<keyof T, Keys>>
from RequireAtLeastOne
becomes { title: string, icon: string}
, which are the unchanged properties of the keys not included in 'click' | 'component'
{ [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>> }[Keys]
from RequireAtLeastOne
becomes
{
component: Required<{ component?: number }> & { click?: number },
click: Required<{ click?: number }> & { component?: number }
}[Keys]
Which becomes
{
component: { component: number, click?: number },
click: { click: number, component?: number }
}['component' | 'click']
Which finally becomes
{component: number, click?: number} | {click: number, component?: number}
The intersection of steps 1 and 2 above
{ title: string, icon: string}
&
({component: number, click?: number} | {click: number, component?: number})
simplifies to
{ title: string, icon: string, component: number, click?: number}
| { title: string, icon: string, click: number, component?: number}
Upvotes: 380
Reputation: 58632
I ended up doing:
export interface MenuItem {
title: string;
icon: string;
}
export interface MenuItemComponent extends MenuItem{
component: any;
}
export interface MenuItemClick extends MenuItem{
click: any;
}
Then I used:
appMenuItems: Array<MenuItemComponent|MenuItemClick>;
But was hoping there was a way to model it with a single interface.
Upvotes: 9
Reputation: 92334
An alternative without multiple interfaces is
export type MenuItem = {
title: string;
component: any;
icon: string;
} | {
title: string;
click: any;
icon: string;
};
const item: MenuItem[] = [
{ title: "", icon: "", component: {} },
{ title: "", icon: "", click: "" },
// Shouldn't this error out because it's passing a property that is not defined
{ title: "", icon: "", click: "", component: {} },
// Does error out :)
{ title: "", icon: "" }
];
I've asked a similar question at How to create a Partial-like that requires a single property to be set
The above could be simplified, but it may or may not be easier to read
export type MenuItem = {
title: string;
icon: string;
} & (
{component: any} | {click: string}
)
Note that none of these prevent you from adding both because TypeScript does allow extra properties on objects that use AND/OR See https://github.com/Microsoft/TypeScript/issues/15447
Upvotes: 21
Reputation: 48337
Not with a single interface, since types have no conditional logic and can't depend on each other, but you can by splitting the interfaces:
export interface BaseMenuItem {
title: string;
icon: string;
}
export interface ComponentMenuItem extends BaseMenuItem {
component: any;
}
export interface ClickMenuItem extends BaseMenuItem {
click: any;
}
export type MenuItem = ComponentMenuItem | ClickMenuItem;
Upvotes: 222