giao diện typecript yêu cầu một trong hai thuộc tính tồn tại


109

Tôi đang cố gắng tạo một giao diện có thể có

export interface MenuItem {
  title: string;
  component?: any;
  click?: any;
  icon: string;
}
  1. Có cách nào để yêu cầu componenthoặc clickthiết lập không
  2. Có cách nào để yêu cầu không thể đặt cả hai thuộc tính không?

Câu trả lời:


94

Không phải với một giao diện duy nhất, vì các kiểu không có logic điều kiện và không thể phụ thuộc vào nhau, nhưng bạn có thể bằng cách tách các giao diện:

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;

Hấp dẫn. Tôi chưa bao giờ xuất một "loại" trước đây. Vì vậy, nó đơn giản có nghĩa là MenuItem có thể là một hay khác?
Nix

Tôi thích cái này hơn cái tôi đã làm dưới đây. Liên kết đã giúp tôi hiểu "loại" stackoverflow.com/a/36783051/256793
Nix

Có: TS có ANDORtoán tử cho các loại của nó. Nếu bạn đang làm điều gì đó như thế này, OR có thể rất tuyệt khi cung cấp cho bạn một tên được thu thập duy nhất sau khi khai báo một loạt các loại riêng lẻ.
ssube

7
Xin chào các bạn, làm thế nào để bạn buộc phải chọn giữa một trong các giao diện, nhưng không phải cả hai? Với loại đó, một đối tượng có cả hai componentclick sẽ không mắc lỗi ...
Daniel Ramos

4
@DanielRamos bạn có thể thêm click?: nevervào ComponentMenuItemcomponent?: nevertrên ClickMenuItem.
ethan.roday

161

Với sự trợ giúp của Excludekiểu đã được thêm vào TypeScript 2.8, một cách tổng quát để yêu cầu ít nhất một trong các thuộc tính được cung cấp là:

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]

Và cách một phần nhưng không tuyệt đối để yêu cầu cung cấp một và chỉ một là:

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]

Đây là một liên kết sân chơi TypeScript hiển thị cả hai hoạt động.

Lưu ý RequireOnlyOnelà TypeScript không phải lúc nào cũng biết mọi thuộc tính sẽ tồn tại trong thời gian chạy. Vì vậy, rõ ràng là RequireOnlyOnekhông thể làm bất cứ điều gì để ngăn chặn các thuộc tính phụ mà nó không biết. Tôi đã cung cấp một ví dụ về cách RequireOnlyOnecó thể bỏ lỡ những thứ ở cuối liên kết sân chơi.

Tổng quan nhanh về cách nó hoạt động bằng cách sử dụng ví dụ sau:

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

type ClickOrComponent = RequireAtLeastOne<MenuItem, 'click' | 'component'>
  1. Pick<T, Exclude<keyof T, Keys>>từ RequireAtLeastOnetrở thành { title: string, icon: string}, là các thuộc tính không thay đổi của các khóa không có trong'click' | 'component'

  2. { [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>> }[Keys]từ RequireAtLeastOnetrở thành

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

    Trở thành

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

    Cuối cùng trở thành

    {component: number, click?: number} | {click: number, component?: number}
    
  3. Giao điểm của bước 1 và bước 2 ở trên

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

    đơn giản hóa thành

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

6
Cảm ơn bạn rất nhiều cho các ví dụ mô tả. Thực sự nhiều thông tin.
Eduard

1
Cảm ơn vì một phản hồi rất nhiều thông tin và được hình thành tốt. Tôi đang chơi với ví dụ này để tôi có thể hiểu rõ hơn về nó. Có vẻ như nếu bạn cung cấp componentclickmột kiểu khác thì anymột đối tượng có cả hai ít nhất một tập hợp các thuộc tính hợp lệ sẽ vượt qua. Tôi cho rằng đó là do cách loại giảm thành { title: string, icon: string, component: any} | { title: string, icon: string, click: any}Mà nói rằng loại là 3 thuộc tính thay vì 4 với một là tùy chọn. Tôi đang cố gắng tìm tài liệu về việc sử dụng mảng trong ký hiệu bản đồ nhưng không thể.
Sam R

5
Có ai có thể giải thích những gì gạch nối (dấu trừ) trong biểu thức [K in keys]-?
Lopsided

2
@Lopsided Nó loại bỏ bất kỳ công cụ sửa đổi tùy chọn nào có thể đã tồn tại Ktrong đối tượng gốc T. Xem stackoverflow.com/questions/49655419/… Về mục đích [K in Keys]-?:cụ thể của nó : Tôi vừa thực hiện một số thử nghiệm và có vẻ như nó thực sự không tạo ra sự khác biệt trong kết quả cuối cùng, nhưng tôi đưa nó vào chỉ để chắc chắn rằng nó RequireAtLeastOnehoạt động theo cùng một cách bất kể các thuộc tính được chỉ định cho Keysban đầu là tùy chọn hay không.
KPD

2
Lưu ý: Đây là một phần của chính thức của Microsoft Azure nguyên cảo SDK docs.microsoft.com/en-us/javascript/api/@azure/...
Simon_Weaver

24

Không cần thiết phải sử dụng các loại điều kiện phức tạp . Chúng tôi có thể làm cho nó đơn giản hơn:

  1. Có cách nào để yêu cầu thành phần hoặc nhấp chuột được thiết lập không? (bao gồm 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: {} } // ✔

Một kiểu liên hợp ( |) tương ứng với bao gồm OR. Nó được giao với các thuộc tính không điều kiện.

  1. Có cách nào để yêu cầu không thể đặt cả hai thuộc tính không? (Độc quyền 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

Về cơ bản, một trong hai component hoặc click có thể được đặt, cái kia không bao giờ được thêm vào cùng một lúc. TS có thể làm cho một loại công đoàn bị phân biệt ra khỏi MenuItemXor, mà tương ứng với XOR.

Lưu ý : Điều XORkiện MenuItemXornày là không thể với câu trả lời được chấp nhận .


Mẫu mã để chơi với


2
câu trả lời thực sự trôi chảy
Ivan Caceres

1
Làm thế nào để giải quyết testOr.click;khi trả về lỗi Property 'click' does not exist on type 'MenuItemOr'.?
Eduardo Dallmann

2
@EduardoDallmann, bạn có thể sử dụng intoán tử để kiểm tra, nếu một trong hai thuộc tính tồn tại trên đối tượng ( Playground )
ford04

Câu trả lời rất hay! Nhưng bạn có thể giải thích nó hoạt động như thế nào không? Tôi gặp khó khăn khi hiểu union ( |) chỉ với một toán hạng bên phải. Cảm ơn bạn
NorTicUs

@NorTicUs Nếu bạn có ý nghĩa hàng đầu |bên trong & ( ... )của MenuItemXor: đây chỉ là một phím tắt cho các nhà điều hành công đoàn cho loại tốt hơn định dạng / tách trên đường. Thích ở đây - không liên quan đến phép thuật :)
ford04

13

Một giải pháp thay thế không có nhiều giao diện là

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

Tôi đã hỏi một câu hỏi tương tự tại Cách tạo một Phần giống như vậy yêu cầu một thuộc tính duy nhất được đặt

Phần trên có thể được đơn giản hóa, nhưng nó có thể dễ đọc hơn hoặc không

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

Lưu ý rằng không có cách nào trong số này ngăn bạn thêm cả hai vì TypeScript cho phép các thuộc tính bổ sung trên các đối tượng sử dụng VÀ / HOẶC Xem https://github.com/Microsoft/TypeScript/issues/15447


1
Bạn có thể sử dụng &và trích xuất titleiconvào một loại riêng biệt. :)
Robert Koritnik

5

Tôi đã kết thúc:

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

export interface MenuItemComponent extends MenuItem{
  component: any;
}

export interface MenuItemClick extends MenuItem{
  click: any;
}

Sau đó, tôi sử dụng:

 appMenuItems: Array<MenuItemComponent|MenuItemClick>;

Nhưng đã hy vọng có một cách để mô hình hóa nó với một giao diện duy nhất.


1
Trên thực tế Microsoft cho thấy nó nên là:<MenuItemComponent|MenuItemClick>[]
Loolooii

3

Tôi thích sử dụng Pickcùng với một loại cơ sở bao gồm tất cả các thuộc tính để thiết lập các loại yêu cầu điều kiện này.

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

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

Điều này là sạch sẽ và cũng linh hoạt. Bạn có thể tùy ý phức tạp với các yêu cầu của mình, khẳng định những thứ như "yêu cầu tất cả các thuộc tính, chỉ hai thuộc tính này hoặc chỉ một thuộc tính này", v.v. trong khi vẫn giữ cho khai báo của bạn đơn giản và dễ đọc.


1
giao diện không thể "=" một kiểu. Nếu điều này là export interface MenuItem = ...nó sẽ hợp lệ.
J'e

2

Tôi sử dụng cái này:

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

Sử dụng:

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

Điều này làm fieldAfieldB bắt buộc .


0

Để chỉ mở rộng các câu trả lời thú vị ở trên! Và dành cho những người hạ cánh ở đây trong khi tìm kiếm phiên bản Một phần với khả năng yêu cầu! Đây là một đoạn tôi đã thực hiện!

PartialReq

Bạn muốn có một phần của giao diện, nhưng đồng nghĩa với việc yêu cầu một số trường! Đây là cách nó được thực hiện

export type PartialReq<T, Keys extends keyof T = keyof T> =
    Pick<Partial<T>, Exclude<keyof T, Keys>>
    & {
        [K in Keys]: T[K]
    };

Sử dụng ví dụ

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

Ở đây bạn có thể thấy rằng nó hoạt động hoàn hảo

nhập mô tả hình ảnh ở đây

Nếu yêu cầu không được cung cấp

nhập mô tả hình ảnh ở đây

CẬP NHẬT: Đối với cách sử dụng mà tôi ngụ ý ở trên, câu trả lời tốt hơn

Tôi vừa lướt qua tài liệu và tìm thấy Omit .

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

Tôi đến để thêm nó. Nhưng trước khi làm, tôi chỉ thấy câu trả lời thú vị này. Nó bao gồm tất cả:

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

Chỉ cần kiểm tra nó ra! Nó chỉ ra cách thực hiện cho tất cả các phiên bản khác nhau của Typescript! Và vì lợi ích của việc không lặp lại! Đi và kiểm tra!

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.