问题
Let's say we have a schema like this (I borrowed the OpenAPI 3.0 format but I think the intention is clear):
{
"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" }
]
}
}
}
}
Based on this schema and the docs I read so far I would express types OneOfSample
and AllOfSample
like this:
type OneOfSample = HasName | HasEmail // Union type
type AllOfSample = HasName & HasEmail // Intersection type
But how would I express type AnyOfSample
? Based on this page: https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/ I would think of something like this:
type AnyOfSample = HasName | HasEmail | (HasName & HasEmail)
The question is how do I correctly express the anyOf type in JSON schemas in typescript?
回答1:
In the following I assume we're using TS v3.1:
It looks like "OneOf" means "must match exactly one", while "AnyOf" means "must match at least one". It turns out that "at least one" is a more basic concept and corresponds to the union operation ("inclusive or") represented by the |
symbol. Therefore, the answer to your question as stated it just:
type AnyOfSample = HasName | HasEmail // Union type
Note that a further union with the intersection doesn't change things:
type AnyOfSample = HasName | HasEmail | (HasName & HasEmail) // same thing
because a union can only possibly add elements, and all of the elements of HasName & HasEmail
are already present in HasName | HasEmail
.
Of course this means you have an incorrect definition for OneOfSample
. This operation is more like a disjunctive union ("exclusive or"), although not exactly because when you have three or more sets, the usual definition of a disjunctive union means "matches an odd number", which is not what you want. As an aside, I can't find a widely used name for the type of disjunctive union we're talking about here, although here's an interesting paper that discusses it.
So, how do we represent "matches exactly one" in TypeScript? This isn't straightforward because it is most easily built in terms of the negation or subtraction of types, which TypeScript can't currently do. That is, you want to say something like:
type OneOfSample = (HasName | HasEmail) & Not<HasName & HasEmail>; // Not doesn't exist
but there is no Not
that works here. All you can do, therefore, is some kind of workaround... so what's possible? You can tell TypeScript that a type may not have a particular property. For example the type NoFoo
may not have a foo
key:
type ProhibitKeys<K extends keyof any> = {[P in K]?: never};
type NoFoo = ProhibitKeys<'foo'>; // becomes {foo?: never};
And you can take a list of key names and remove key names from another list (that is, subtraction of string literals), using conditional types:
type Subtract = Exclude<'a'|'b'|'c', 'c'|'d'>; // becomes 'a'|'b'
This lets you do something like the following:
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];
In this case, ExactlyOneOf
expects a tuple of types, and will represent a union of each element of the tuple explicitly prohibiting keys from other types. Let's see it in action:
type HasName = { name: string };
type HasEmail = { email: string };
type OneOfSample = ExactlyOneOf<[HasName, HasEmail]>;
If we inspect OneOfSample
with IntelliSense, it is:
type OneOfSample = (HasEmail & ProhibitKeys<"name">) | (HasName & ProhibitKeys<"email">);
which is saying "either a HasEmail
with no name
property, or a HasName
with no email
property. Does it work?
const okayName: OneOfSample = { name: "Rando" }; // okay
const okayEmail: OneOfSample = { email: "rando@example.com" }; // okay
const notOkay: OneOfSample = { name: "Rando", email: "rando@example.com" }; // error
Looks like it.
The tuple syntax lets you add three or more types:
type HasCoolSunglasses = { shades: true };
type AnotherOneOfSample = ExactlyOneOf<[HasName, HasEmail, HasCoolSunglasses]>;
This inspects as
type AnotherOneOfSample = (HasEmail & ProhibitKeys<"name" | "shades">) |
(HasName & ProhibitKeys<"email" | "shades">) |
(HasCoolSunglasses & ProhibitKeys<"email" | "name">)
which, as you see, correctly distributes the prohibited keys around.
There are other ways to do it, but that's how I'd proceed. It is a workaround and not a perfect solution because there are situations it doesn't handle properly, such as two types with the same keys whose properties are different types:
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
In the above, ExactlyOneOf<>
fails to recurse down into the properties of the pet
property to make sure that it is not both a Cat
and a Dog
. This can be addressed, but it starts getting more complicated than you probably want. There are other edge cases too. It depends on what you need.
Anyway, hope that helps. Good luck!
回答2:
Actually, the idea of expressing a JSON Schema as a type definition is a paradigm mismatch. JSON Schema isn't designed for that kind of thing. It's trying to hammer a round peg into a square hole. It's never going to fit quite right.
JSON Schema is designed to translate into a function that can be used to validate a JSON document.
来源:https://stackoverflow.com/questions/52836812/how-do-json-schemas-anyof-type-translate-to-typescript