La interfaz mecanografiada requiere una de dos propiedades para existir


Respuestas:


94

No con una sola interfaz, ya que los tipos no tienen lógica condicional y no pueden depender entre sí, pero puede dividir las 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;

Interesante. Nunca antes había exportado un "tipo". Entonces, ¿simplemente significa que MenuItem puede ser uno u otro?
— Nix

Me gusta esto más que el que hice a continuación. Enlace que me ayudó a entender el "tipo" stackoverflow.com/a/36783051/256793
— Nix

Sí: TS tiene ANDy ORoperadores para sus tipos. Si está haciendo algo como esto, el OR puede ser bueno para darle un solo nombre recopilado después de declarar un montón de tipos individuales.
— ssube

7
Hola chicos, ¿cómo se fuerza a elegir entre una de las interfaces, pero no ambas? Con ese tipo, un objeto con ambos componenty click no cometerá un error ...
— Daniel Ramos

4
@DanielRamos se puede añadir click?: neveren ComponentMenuItemy component?: neveren ClickMenuItem.
— ethan.roday

161

Con la ayuda del Excludetipo que se agregó en TypeScript 2.8, se proporciona una forma generalizable de requerir al menos una de un conjunto de propiedades:

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]

Y una forma parcial pero no absoluta de requerir que se proporcione uno y solo uno es:

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]

Aquí hay un enlace al patio de juegos de TypeScript que muestra ambos en acción.

La advertencia RequireOnlyOnees que TypeScript no siempre conoce en tiempo de compilación cada propiedad que existirá en tiempo de ejecución. Entonces, obviamente RequireOnlyOne, no puede hacer nada para evitar propiedades adicionales que no conoce. Proporcioné un ejemplo de cómo se RequireOnlyOnepueden perder cosas al final del enlace del patio de recreo.

Una descripción general rápida de cómo funciona con el siguiente ejemplo:

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

type ClickOrComponent = RequireAtLeastOne<MenuItem, 'click' | 'component'>
  1. Pick<T, Exclude<keyof T, Keys>>de se RequireAtLeastOneconvierte { title: string, icon: string}, que son las propiedades inalteradas de las claves no incluidas en'click' | 'component'

  2. { [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>> }[Keys]de se RequireAtLeastOneconvierte en

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

    Que se convierte en

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

    Que finalmente se convierte en

    {component: number, click?: number} | {click: number, component?: number}
    
  3. La intersección de los pasos 1 y 2 anteriores

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

    simplifica a

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

6
Muchas gracias por los ejemplos descriptivos. Realmente informativo.
— Eduard

1
Gracias por una respuesta extremadamente informativa y bien formada. He estado jugando con este ejemplo para poder entenderlo mejor. Parece que si da componenty clickescribe otro any, pasará un objeto con al menos un conjunto de propiedades válidas. Supongo que se debe a la forma en que el tipo se reduce a { title: string, icon: string, component: any} | { title: string, icon: string, click: any}Lo que establece que el tipo es 3 propiedades en lugar de 4, siendo una opcional. Estoy tratando de encontrar documentación sobre el uso de la matriz en la notación del mapa, pero no puedo.
— Sam R

5
¿Alguien puede explicar qué hace el guión (signo menos) en la expresión [K in keys]-?
— Desequilibrado

2
@Lopsided Elimina los modificadores opcionales que podrían haber existido Ken el objeto original T. Ver stackoverflow.com/questions/49655419/… En cuanto a su propósito [K in Keys]-?:específicamente: Acabo de hacer algunas pruebas y parece que en realidad no hace una diferencia en el resultado final, pero lo puse solo para asegurarme de que se RequireAtLeastOnecomporta de la misma manera independientemente de si las propiedades especificadas para Keyseran originalmente opcionales o no.
— KPD

2
Nota: Esto es parte del SDK de Microsoft Azure Typescript
— Simon_Weaver

23

No es necesario utilizar tipos condicionales complejos . Podemos hacerlo más sencillo:

  1. ¿Hay alguna forma de requerir que se establezca un componente o un clic? (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: {} } // ✔

Un tipo de unión ( |) corresponde a inclusive OR. Se cruza con las propiedades no condicionales.

  1. ¿Hay alguna forma de exigir que no se puedan establecer ambas propiedades? (Exclusivo 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

Básicamente bien component o click se puede ajustar, el otro debe nunca se pueden añadir al mismo tiempo. TS puede hacer un tipo de unión discriminada de MenuItemXor, lo que corresponde a XOR.

Nota : Esta XORcondición para MenuItemXorno es posible con respuesta aceptada .


Muestra de código para jugar


2
respuesta realmente suave
— Ivan Caceres

1
¿Cómo lidiar con testOr.click;eso devuelve el error Property 'click' does not exist on type 'MenuItemOr'.?
— Eduardo Dallmann

2
@EduardoDallmann puede usar el inoperador para verificar, si alguna de las propiedades existen en el objeto ( Patio de juegos )
— ford04

¡Buena respuesta! Pero, ¿puedes explicar cómo funciona? Me cuesta entender la unión ( |) con solo un operando correcto. Gracias
— NorTicUs

@NorTicUs Si te refieres al |interior principal & ( ... )de MenuItemXor: esto es solo un atajo para el operador de unión para un mejor formato de tipo / separación sobre líneas. Como aquí , no hay magia involucrada :)
— Ford04

13

Una alternativa sin múltiples interfaces es

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

Hice una pregunta similar en Cómo crear un parcial que requiere que se establezca una sola propiedad

Lo anterior podría simplificarse, pero puede o no ser más fácil de leer

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

Tenga en cuenta que ninguno de estos le impide agregar ambos porque TypeScript permite propiedades adicionales en los objetos que usan Y / O Consulte https://github.com/Microsoft/TypeScript/issues/15447


1
Puede usar &y extraer titley iconen un tipo separado. :)
— Robert Koritnik

5

Terminé haciendo:

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

export interface MenuItemComponent extends MenuItem{
  component: any;
}

export interface MenuItemClick extends MenuItem{
  click: any;
}

Entonces usé:

 appMenuItems: Array<MenuItemComponent|MenuItemClick>;

Pero esperaba que hubiera una forma de modelarlo con una sola interfaz.


1
En realidad, Microsoft sugiere que debería ser:<MenuItemComponent|MenuItemClick>[]
— Loolooii

3

Me gusta usar Pickjunto con un tipo base que incluye todas las propiedades para establecer este tipo de requisitos condicionales.

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

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

Esto es limpio y también flexible. Puede volverse arbitrariamente complejo con sus requisitos, afirmando cosas como "requieren todas las propiedades, solo estas dos propiedades, o solo esta propiedad" y así sucesivamente mientras mantiene su declaración simple y legible.


1
las interfaces no pueden "=" un tipo. Si esto fuera export interface MenuItem = ...así, sería válido.
— J'e

2

Yo uso esto:

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

Uso:

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

Esto hace fieldAy fieldB requiere .


0

¡Se extiende a las geniales respuestas anteriores! ¡Y para las personas que aterrizan aquí mientras buscan una versión parcial con la capacidad requerida! ¡Aquí un fragmento que hice para tomar!

PartialReq

Desea tener un Parcial de una interfaz, pero mientras tanto, ¡requiere algunos de los campos! Aquí como se hace

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

Ejemplo de uso

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

Aquí puedes ver que funciona perfectamente

ingrese la descripción de la imagen aquí

Si el requerido no se proporciona

ingrese la descripción de la imagen aquí

ACTUALIZACIÓN: Para el uso que implicaba anteriormente aquí, una mejor respuesta

Pasé por el doc y encontré a Omit .

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

Vine a agregarlo. Pero antes de hacerlo, acabo de ver esta genial respuesta. Cubre todo:

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

¡Solo compruébalo! ¡Muestra cómo hacerlo para todas las diferentes versiones de Typecript! ¡Y por no repetir! ¡Ve y comprueba!

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.