La interfaz mecanografiada requiere que exista una de dos propiedades.
Estoy intentando crear una interfaz que podría tener
export interface MenuItem {
title: string;
component?: any;
click?: any;
icon: string;
}
- ¿Hay alguna manera de exigir
component
oclick
establecer? - ¿Hay alguna manera de exigir que no se puedan establecer ambas propiedades?
Con la ayuda del Exclude
tipo 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 exigir 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 área de juegos de TypeScript que muestra ambos en acción.
La advertencia RequireOnlyOne
es que TypeScript no siempre conoce en el momento de la compilación todas las propiedades que existirán en el tiempo de ejecución. Obviamente, RequireOnlyOne
no puedo hacer nada para evitar propiedades adicionales que no conoce. Proporcioné un ejemplo de cómo RequireOnlyOne
se pueden perder cosas al final del enlace del patio de juegos.
Una descripción general rápida de cómo funciona utilizando el siguiente ejemplo:
interface MenuItem {
title: string;
component?: number;
click?: number;
icon: string;
}
type ClickOrComponent = RequireAtLeastOne<MenuItem, 'click' | 'component'>
Pick<T, Exclude<keyof T, Keys>>
fromRequireAtLeastOne
comes{ title: string, icon: string}
, que son las propiedades sin cambios de las claves no incluidas en'click' | 'component'
{ [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>> }[Keys]
deRequireAtLeastOne
se convierte{ component: Required<{ component?: number }> & { click?: number }, click: Required<{ click?: number }> & { component?: number } }[Keys]
que se convierte
{ component: { component: number, click?: number }, click: { click: number, component?: number } }['component' | 'click']
que finalmente se convierte
{component: number, click?: number} | {click: number, component?: number}
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}
No con una única interfaz, ya que los tipos no tienen lógica condicional y no pueden depender unos de otros, pero puedes hacerlo dividiendo 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;
Hay una solución más sencilla. No es necesario depender de tipos condicionalesany
complejos (ver respuesta) :
- ¿Hay alguna manera de requerir que se configure un componente o un clic? (Inclusivo
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 inclusivo OR
. Se cruza con las propiedades no condicionales.
Utilice el in
operador para limitar el valor a uno de los componentes:
if ("click" in testOr) testOr.click // works
- ¿Hay alguna manera 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 set
Básicamente se puede configurar uno component
o , el otro nunca debe agregarse al mismo tiempo. TS puede hacer un tipo de unión discriminada a partir de , que corresponde a .click
MenuItemXor
XOR
Esta XOR
condición MenuItemXor
no es posible con una respuesta aceptada .
Patio de juegos
1 Técnicamente, prop?: never
se resuelve en prop?: undefined
, aunque el primero se utiliza a menudo a modo de ilustración.
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 tipo parcial que requiere que se establezca una sola propiedad
Lo anterior podría simplificarse, pero puede que sea más fácil de leer o no.
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 objetos que usan Y/O Consulte https://github.com/Microsoft/TypeScript/issues/15447