Как тип anyOf схемы JSON переводится на машинопись? - PullRequest
0 голосов
/ 16 октября 2018

Скажем, у нас есть такая схема (я позаимствовал формат OpenAPI 3.0, но я думаю, что цель ясна):

{
  "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" }
        ]
      }
    }
  }
}

Основываясь на этой схеме и документах, которые я прочитал, я бы выразилтипы OneOfSample и AllOfSample вот так:

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

Но как бы я выразил тип AnyOfSample?Основываясь на этой странице: https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/ Я бы подумал о чем-то вроде этого:

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

Вопрос в том, как правильно выразить тип anyOf в схемах JSON в машинописном наборе

Ответы [ 2 ]

0 голосов
/ 17 октября 2018

На самом деле, идея выражения JSON-схемы как определения типа является несоответствием парадигмы.Схема JSON не предназначена для такого рода вещей.Он пытается забить круглый колышек в квадратное отверстие.Это никогда не подойдет совсем правильно.

Схема JSON предназначена для преобразования в функцию, которую можно использовать для проверки документа JSON.

0 голосов
/ 16 октября 2018

В следующем я предполагаю, что мы используем TS v3.1:

Похоже, что "OneOf" означает "должен соответствовать точно один", тогда как "AnyOf" означает "долженсоответствует как минимум один ".Оказывается, что «хотя бы один» является более базовой концепцией и соответствует операции union включительно или »), представленной символом |.Таким образом, ответ на ваш вопрос таков:

type AnyOfSample = HasName | HasEmail // Union type

Обратите внимание, что дальнейшее объединение с пересечением ничего не меняет:

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

, поскольку объединение может только возможноДобавьте элементы, и все элементы HasName & HasEmail уже присутствуют в HasName | HasEmail.


Конечно, это означает, что у вас есть неправильное определение для OneOfSample.Эта операция больше похожа на дизъюнктивный союз ( "exclusive или" ), хотя не совсем так, потому что, когда у вас есть три или более наборов, обычное определение средства дизъюнктивного объединения "соответствуетнечетное число ", а это не то, что вы хотите.Кроме того, я не могу найти широко используемого названия для типа дизъюнктивного союза, о котором мы здесь говорим, хотя вот интересная статья , в которой это обсуждается.

Итак, какмы представляем "соответствует ровно одному" в TypeScript?Это не просто, потому что его легче всего построить с помощью отрицания или вычитания типов, чего в настоящее время TypeScript не может сделать.То есть вы хотите сказать что-то вроде:

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

, но здесь не работает Not.Поэтому все, что вы можете сделать, это какой-то обходной путь ... так что же возможно?Вы можете сообщить TypeScript, что тип может не иметь определенного свойства.Например, тип NoFoo может не иметь ключа foo:

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

И вы можете взять список имен ключей и удалить имена ключей из другого списка (то есть, вычитание строковых литералов), используя условные типы:

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

Это позволяет вам сделать что-то вроде следующего:

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

В этом случае ExactlyOneOf ожидает кортеж типови будет представлять объединение каждого элемента кортежа, явно запрещающее ключи других типов.Давайте посмотрим на это в действии:

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

Если мы проверяем OneOfSample с IntelliSense, то это:

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

, который говорит "либо HasEmail без nameсвойство или HasName без свойства email. Работает ли оно?

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

Похоже на это.

Синтаксис кортежа позволяет добавлять три или более типов:

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

Это проверяется как

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

, который, как вы видите, правильно распределяет запрещенные ключи вокруг.


Есть и другие способы сделать это, ноя бы так и поступил. Это обходной путь, а не идеальное решение, потому что есть ситуации, которые он не обрабатывает должным образом, например, два типа с одинаковыми ключами, свойства которых имеют разные типы:

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

В приведенном выше примере ExactlyOneOf<> не может вернуться к свойствам свойства pet, чтобы убедиться, что оно не является одновременно Cat и Dog. Это может быть решено, но оно становится более сложным, чемВы, вероятно, хотите. Есть и другие крайние случаи. Это зависит от того, что выред.

В любом случае, надеюсь, это поможет.Удачи!

...