Digamos que tenemos un esquema como este (tomé prestado el formato OpenAPI 3.0 pero creo que la intención es clara):

{
  "components": {
    "schemas": {
      "HasName": {
        "type": "object",
        "properties": {
          "name": { "type": "string" }
        }
      },
      "HasEmail": {
        "type": "object",
        "properties": {
          "email": { "type": "string" }
        }
      },
      "OneOfSample": {
        "oneOf": [
          { "$ref": "#/components/schemas/HasName" },
          { "$ref": "#/components/schemas/HasEmail" }
        ]
      },
      "AllOfSample": {
        "allOf": [
          { "$ref": "#/components/schemas/HasName" },
          { "$ref": "#/components/schemas/HasEmail" }
        ]
      },
      "AnyOfSample": {
        "anyOf": [
          { "$ref": "#/components/schemas/HasName" },
          { "$ref": "#/components/schemas/HasEmail" }
        ]
      }
    }
  }
}

Según este esquema y los documentos que leí hasta ahora, expresaría los tipos OneOfSample y AllOfSample así:

type OneOfSample = HasName | HasEmail // Union type
type AllOfSample = HasName & HasEmail // Intersection type

Pero, ¿cómo expresaría el tipo AnyOfSample? Basado en esta página: https://swagger.io/ docs / Specification / data-models / oneof-anyof-allof-not / Pensaría en algo como esto:

type AnyOfSample = HasName | HasEmail | (HasName & HasEmail)

La pregunta es ¿cómo expreso correctamente el tipo anyOf en esquemas JSON en mecanografiado ?

3
Balázs Édes 16 oct. 2018 a las 16:38

2 respuestas

La mejor respuesta

En lo siguiente, supongo que estamos usando TS v3.1:

Parece que "OneOf" significa "debe coincidir exactamente uno", mientras que "AnyOf" significa "debe coincidir al menos uno". Resulta que "al menos uno" es un concepto más básico y corresponde a la unión operación (" inclusive o") representada por el símbolo |. Por lo tanto, la respuesta a su pregunta tal como la indica simplemente:

type AnyOfSample = HasName | HasEmail // Union type

Tenga en cuenta que una unión adicional con la intersección no cambia las cosas:

type AnyOfSample = HasName | HasEmail | (HasName & HasEmail) // same thing

Porque una unión solo puede agregar elementos, y todos los elementos de HasName & HasEmail ya están presentes en HasName | HasEmail.


Por supuesto, esto significa que tiene una definición incorrecta de OneOfSample. Esta operación se parece más a una unión disyuntiva (" exclusivo o "), aunque no exactamente porque cuando tienes tres o más conjuntos, la definición habitual de una unión disyuntiva significa" coincide con un número impar ", que no es lo que quieres. Aparte, no puedo encontrar un nombre ampliamente utilizado para el tipo de unión disyuntiva del que estamos hablando aquí, aunque aquí hay un artículo interesante que lo discute.

Entonces, ¿cómo representamos "coincide exactamente con uno" en TypeScript? Esto no es sencillo porque se construye más fácilmente en términos de negación o < una href = "https://github.com/Microsoft/TypeScript/issues/4183" rel = "nofollow noreferrer"> resta de tipos, que TypeScript no puede hacer actualmente. Es decir, quieres decir algo como:

type OneOfSample = (HasName | HasEmail) & Not<HasName & HasEmail>; // Not doesn't exist

Pero no hay ningún Not que funcione aquí. Todo lo que puede hacer, por lo tanto, es algún tipo de solución ... entonces, ¿qué es posible? puede decirle a TypeScript que un tipo puede no tener una propiedad en particular. Por ejemplo, el tipo NoFoo puede no tener una clave foo:

type ProhibitKeys<K extends keyof any> = {[P in K]?: never}; 
type NoFoo = ProhibitKeys<'foo'>; // becomes {foo?: never};

Y puede tomar una lista de nombres de clave y eliminar nombres de clave de otra lista (es decir, resta de cadenas literales), usando tipos condicionales:

type Subtract = Exclude<'a'|'b'|'c', 'c'|'d'>; // becomes 'a'|'b'

Esto le permite hacer algo como lo siguiente:

type AllKeysOf<T> = T extends any ? keyof T : never; // get all keys of a union
type ProhibitKeys<K extends keyof any> = {[P in K]?: never }; // from above
type ExactlyOneOf<T extends any[]> = {
  [K in keyof T]: T[K] & ProhibitKeys<Exclude<AllKeysOf<T[number]>, keyof T[K]>>;
}[number];

En este caso, ExactlyOneOf espera una tupla de tipos y representará una unión de cada elemento de la tupla que prohíbe explícitamente claves de otros tipos. Veámoslo en acción:

type HasName = { name: string };
type HasEmail = { email: string };
type OneOfSample = ExactlyOneOf<[HasName, HasEmail]>;

Si inspeccionamos OneOfSample con IntelliSense, es:

type OneOfSample = (HasEmail & ProhibitKeys<"name">) | (HasName & ProhibitKeys<"email">);

Que dice "una HasEmail sin propiedad name o una HasName sin propiedad email. ¿Funciona?

const okayName: OneOfSample = { name: "Rando" }; // okay
const okayEmail: OneOfSample = { email: "rando@example.com" }; // okay
const notOkay: OneOfSample = { name: "Rando", email: "rando@example.com" }; // error

Lo parece.

La sintaxis de tupla le permite agregar tres o más tipos:

type HasCoolSunglasses = { shades: true };
type AnotherOneOfSample = ExactlyOneOf<[HasName, HasEmail, HasCoolSunglasses]>;

Esto inspecciona como

type AnotherOneOfSample = (HasEmail & ProhibitKeys<"name" | "shades">) | 
  (HasName & ProhibitKeys<"email" | "shades">) | 
  (HasCoolSunglasses & ProhibitKeys<"email" | "name">)

Que, como ves, distribuye correctamente las claves prohibidas.


Hay otras formas de hacerlo, pero así es como procedería. Es una solución alternativa y no una solución perfecta porque hay situaciones que no se manejan correctamente, como dos tipos con las mismas claves cuyas propiedades son tipos diferentes:

declare class Animal { legs: number };
declare class Dog extends Animal { bark(): void };
declare class Cat extends Animal { meow(): void };
type HasPetCat = { pet: Cat };
type HasPetDog = { pet: Dog };
type HasOneOfPetCatOrDog = ExactlyOneOf<[HasPetCat, HasPetDog]>;
declare const abomination: Cat & Dog;
const oops: HasOneOfPetCatOrDog = { pet: abomination }; // not an error

En lo anterior, ExactlyOneOf<> no puede recurrir a las propiedades de la propiedad pet para asegurarse de que no sea a la vez Cat y Dog. Esto se puede solucionar, pero comienza a complicarse más de lo que probablemente desee. También hay otros casos extremos. Depende de lo que necesites.

De todos modos, espero que eso ayude. ¡Buena suerte!

1
jcalz 16 oct. 2018 a las 15:27

En realidad, la idea de expresar un esquema JSON como una definición de tipo es un desajuste de paradigma. JSON Schema no está diseñado para ese tipo de cosas. Está intentando clavar una clavija redonda en un agujero cuadrado. Nunca va a encajar del todo bien.

El esquema JSON está diseñado para traducirse en una función que se puede utilizar para validar un documento JSON.

0
Jason Desrosiers 17 oct. 2018 a las 04:06