typescript接口需要存在两个属性之一

时间:2016-11-09 15:32:22

标签: typescript

我正在尝试创建一个可能有

的界面
export interface MenuItem {
  title: string;
  component?: any;
  click?: any;
  icon: string;
}
  1. 有没有办法要求设置componentclick
  2. 有没有办法要求不能设置这两个属性?

11 个答案:

答案 0 :(得分:49)

借助TypeScript 2.8中添加的Exclude类型,提供了一种需要至少一组属性的通用方法:

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]

要求提供一个且仅提供一个的部分但非绝对的方式是:

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.

RequireOnlyOne的警告是,TypeScript在编译时并不总是知道运行时将存在的每个属性。显然RequireOnlyOne无法做任何事情来阻止它不知道的额外属性。我提供了一个示例,说明RequireOnlyOne如何在游乐场链接的末尾错过任何内容。

使用以下示例快速概述其工作原理:

interface MenuItem {
  title: string;
  component?: number;
  click?: number;
  icon: string;
}

type ClickOrComponent = RequireAtLeastOne<MenuItem, 'click' | 'component'>
    来自Pick<T, Exclude<keyof T, Keys>>
  1. RequireAtLeastOne变为{ title: string, icon: string},这是未包含在'click' | 'component'

  2. 中的密钥的未更改属性 来自{ [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>> }[Keys]
  3. RequireAtLeastOne变为

    { 
        component: Required<{ component?: number }> & { click?: number }, 
        click: Required<{ click?: number }> & { component?: number } 
    }[Keys]
    

    哪个成为

    {
        component: { component: number, click?: number },
        click: { click: number, component?: number }
    }['component' | 'click']
    

    最终成为

    {component: number, click?: number} | {click: number, component?: number}
    
  4. 上面步骤1和2的交集

    { title: string, icon: string} 
    & 
    ({component: number, click?: number} | {click: number, component?: number})
    

    简化为

    { title: string, icon: string, component: number, click?: number} 
    | { title: string, icon: string, click: number, component?: number}
    

答案 1 :(得分:26)

无需使用complex conditional types。我们可以使其更简单:

  1. 是否可以要求设置组件或单击? (包括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: {} } // ✔

union type|)对应于包含的OR。它是intersected,具有非条件属性。

  1. 是否有一种方法不能要求同时设置两个属性? (独家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 are set

基本上 component click都可以设置,另一个never应该同时添加。 TS可以从MenuItemXor中产生discriminated union type,这与XOR相对应。

注意accepted answer无法实现XOR的这种MenuItemXor条件。


Code sample to play around with

答案 2 :(得分:25)

不使用单个接口,因为类型没有条件逻辑且不能相互依赖,但您可以通过拆分接口:

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;

答案 3 :(得分:5)

没有多个接口的替代方案是

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: "" }
];

我在How to create a Partial-like that requires a single property to be set

问了一个类似的问题

以上内容可以简化,但可能更容易阅读

export type MenuItem = {
  title: string;
  icon: string;
} & (
 {component: any} | {click: string}
)

请注意,这些都不会阻止您添加两者,因为TypeScript确实允许使用AND / OR的对象的额外属性请参阅https://github.com/Microsoft/TypeScript/issues/15447

答案 4 :(得分:2)

我使用这个:

type RequireField<T, K extends keyof T> = T & Required<Pick<T, K>>

用法:

let a : RequireField<TypeA, "fieldA" | "fieldB">;

这使得fieldAfieldB必需的

答案 5 :(得分:1)

我最终做了:

export interface MenuItem {
  title: string;
  icon: string;
}

export interface MenuItemComponent extends MenuItem{
  component: any;
}

export interface MenuItemClick extends MenuItem{
  click: any;
}

然后我用了:

 appMenuItems: Array<MenuItemComponent|MenuItemClick>;

但希望有一种方法可以用单一界面对其进行建模。

答案 6 :(得分:1)

我喜欢将Pick与包含所有属性的基本类型一起使用来建立这些条件要求。

interface MenuItemProps {
  title: string;
  component: any;
  click: any;
  icon: string;
}

export interface MenuItem =
  Pick<MenuItemProps, "title" | "icon" | "component"> |
  Pick<MenuItemProps, "title" | "icon" | "click">

这既干净又灵活。您可以随心所欲地满足您的要求,断言诸如“要求所有属性,仅这两个属性或仅此一个属性”之类的事情,等等,同时保持声明的简单性和可读性。

答案 7 :(得分:0)

(回答“胡安·门德斯”更像是一个问题而不是答案)?

<i class="fa fa-exclamation-triangle"></i>

我的想法(intellij / webstorm)允许我声明类型而没有错误,但是抱怨如果引用组件或单击。我猜这是正确的,因为不能保证它在那里。 所以我必须像这样引用它

<i class="fa fa-exclamation-triangle" style="color:red"></i>

这似乎并不能阻止我这样做

export type MenuItemCommon = {
  title: string;
  icon: string;
}

export type Component = {
    component: int;
}

export type Click = {
    click: string;
}

export type MenuItem = MenuItemCommon & (Component | Click)

即同时指定两个属性

答案 8 :(得分:0)

仅对以上很酷的答案进行扩展!对于在这里搜索需要功能的 Partial 版本的人!这是我要摘录的摘录!

PartialReq

您希望具有接口的Partial,但同时需要一些字段!这是怎么做的

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
    })
);

在这里您可以看到它运行完美

enter image description here

如果未提供要求

enter image description here

更新:对于我上面暗示的用法,它是一个更好的答案

我刚浏览文档,发现忽略

https://www.typescriptlang.org/docs/handbook/utility-types.html#omittk

我来添加它。但是在我这样做之前,我只是看到了这个很酷的答案。它涵盖了所有内容:

https://stackoverflow.com/a/48216010/7668448

只需检查一下!它展示了如何为所有不同版本的Typescript做到这一点!并且为了不重复!去检查!

答案 9 :(得分:0)

另一种解决方案:

type RequiredKeys<T, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>;

type MenuItem2 = RequiredKeys<MenuItem, "component" | "click">;

答案 10 :(得分:0)

这种方法结合了 neverOmit。这样做的好处是它易于理解,并且如果您需要添加更多属性也易于更新。

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);

您可以使用 in 来缩小 MenuItem 的实例范围:

const item: MenuItem = {
  title: 'A good title';
  icon: 'fa-plus';
  component: SomeComponent;
};

//...

if('component' in item) {
  const Comp = item.component;
  //...
}