推断构造对象的类型
在我的打字稿应用中,我正在使用 Openapi Spec 并从中构建了一个示例。因此,规格看起来像这样,要简化:
const spec = {
type: 'object',
properties: {
firstName: {
type: 'string',
example: 'Johnbob',
},
lastName: {
type: 'string',
// No example provided
},
age: {
type: 'integer',
example: 30
},
}
}
等。它比这复杂得多,因为OpenAPI还具有更复杂的“关键字”(Oneof
,anyof
),以及数组类型,对象/数组/关键字可以嵌套在彼此之间。
但是从根本上讲,“架构”的任何OpenAPI规范都可以转换为示例对象,包括具有自动生成的虚拟示例。一旦我将其变成一个示例:
{
firstName: 'Johnbob',
lastName: 'Default example string',
age: 30
}
问题:有什么方法可以自动推断/生成生成示例的类型?我知道我可以做到这一点:
const example = {
firstName: 'Johnbob',
lastName: 'Default example string',
age: 30
}
// Ideally, { firstName: string, lastName: string, age: number }
type ExampleType = typeof example;
但是我希望自动键入生成示例功能的返回。目前,它只是伸出双手并返回任何
。
它的基本结构是,它具有Processschema
功能,该功能采用任何模式类型(无论是对象,单一或简单的整数类型),然后递归地通过它“处理”每个孩子。
代码的完整操场此处,以及以下当前的WIP实现:
type PropertyType = "string" | "number" | "integer" | "object" | "boolean" | "array";
type Property =
| BooleanProperty
| NumberProperty
| IntegerProperty
| StringProperty
| ObjectProperty
| ArrayProperty;
interface OneOf {
oneOf: PropertyOrKeyword[];
}
interface AnyOf {
anyOf: PropertyOrKeyword[];
}
type Keyword = OneOf | AnyOf;
type PropertyOrKeyword = Property | Keyword;
type Properties = Record<string, PropertyOrKeyword>;
interface BaseProperty<T> {
type: PropertyType;
enum?: Array<T>;
example?: T;
description?: string;
}
interface BooleanProperty extends BaseProperty<boolean> {
type: "boolean";
}
interface NumberProperty extends BaseProperty<number> {
type: "number";
minimum?: number;
maximum?: number;
format?: "float";
}
interface IntegerProperty extends BaseProperty<number> {
type: "integer";
minimum?: number;
maximum?: number;
}
type StringFormats =
// OpenAPI built-in formats: https://swagger.io/docs/specification/data-models/data-types/#string
| "date"
| "date-time"
| "password"
| "byte"
| "binary"
// But arbitrary others are accepted
| "uuid"
| "email";
interface StringProperty extends BaseProperty<string> {
type: "string";
format?: StringFormats;
/** A string of a regex pattern **/
pattern?: string;
}
interface ObjectProperty extends BaseProperty<Record<string, Property>> {
type: "object";
properties: Record<string, PropertyOrKeyword>;
required?: string[];
title?: string; // If a schema
additionalProperties?: boolean;
}
interface ArrayProperty extends BaseProperty<Array<any>> {
type: "array";
items: PropertyOrKeyword;
}
class Example {
example;
schema: PropertyOrKeyword;
constructor(schema: PropertyOrKeyword) {
this.schema = schema;
const value = this._processSchema(schema);
this.example = value as typeof value;
}
fullExample(description?: string, externalValue?: string) {
return { value: this.example, description, externalValue };
}
/** Traverses schema and builds an example object from its properties */
_processSchema(schema: PropertyOrKeyword) {
if ("oneOf" in schema) {
return this._processSchema(schema.oneOf[0]);
} else if ("anyOf" in schema) {
return this._processSchema(schema.anyOf[0]);
} else if ("items" in schema) {
return [this._processSchema(schema.items)];
} else if ("type" in schema) {
if (schema.type === "object") {
return Object.entries(schema.properties).reduce(
(obj, [key, val]) => ({
[key]: this._processSchema(val as PropertyOrKeyword),
...obj,
}),
{} as object
);
} else {
if (["integer", "number"].includes(schema.type)) this._processSimpleProperty(schema) as number;
if (schema.type === "boolean") this._processSimpleProperty(schema) as boolean;
if (schema.type === "number") this._processSimpleProperty(schema) as number;
return this._processSimpleProperty(schema) as string;
}
}
}
/** Produces a sensible example for non-object properties */
_processSimpleProperty(
prop: NumberProperty | StringProperty | BooleanProperty | IntegerProperty
): number | boolean | string {
// If an example has been explicitly set, return that
if (prop.example) return prop.example;
// If an enum type, grab the first option as an example
if (prop.enum) return prop.enum[0];
// If a string type with format, return a formatted string
if (prop.type === "string" && prop.format) {
return {
uuid: "asdfa-sdfea-wor13-dscas",
date: "1970-01-14",
["date-time"]: "1970-01-14T05:34:58Z+01:00",
email: "[email protected]",
password: "s00pers33cret",
byte: "0d5b4d43dbf25c433a455d4e736684570e78950d",
binary: "01101001001010100111010100100110100d",
}[prop.format] as string;
}
// Otherwise, return a sensible default
return {
string: "Example string",
integer: 5,
number: 4.5,
boolean: false,
}[prop.type];
}
}
const spec: ObjectProperty = {
type: 'object',
properties: {
firstName: {
type: 'string',
example: 'Johnbob',
},
lastName: {
type: 'string',
// No example provided
},
age: {
type: 'integer',
example: 30
},
}
};
const spec: ObjectProperty = {
type: 'object',
properties: {
firstName: {
type: 'string',
example: 'Johnbob',
},
lastName: {
type: 'string',
// No example provided
},
age: {
type: 'integer',
example: 30
},
favoriteThing: {
oneOf: [{
type: 'object',
properties: {
name: {
type: 'string',
example: 'Beer'
},
liters: {
type: 'integer',
example: 1
}
}
},
{
type: 'object',
properties: {
name: {
type: 'string',
example: 'Movie'
},
lengthInMins: {
type: 'integer',
example: 120
}
}
}
]
}
}
};
console.log(new Example(spec).example)
In my TypeScript app, I'm taking an OpenAPI spec and constructing an example from it. So the spec might look something like this, to simplify:
const spec = {
type: 'object',
properties: {
firstName: {
type: 'string',
example: 'Johnbob',
},
lastName: {
type: 'string',
// No example provided
},
age: {
type: 'integer',
example: 30
},
}
}
Etc. It's much more complicated than that, because OpenAPI also has more complicated "keywords" (oneOf
, anyOf
), as well as array types, and objects/arrays/keywords can be nested within one another.
But fundamentally, any OpenAPI specification for a "schema" can be converted into an example object, including with auto-generated dummy examples. The above would become something like this once I've turned it into an example:
{
firstName: 'Johnbob',
lastName: 'Default example string',
age: 30
}
The question: Is there any way to automatically infer/generate the type of the generated example? I know I could do this:
const example = {
firstName: 'Johnbob',
lastName: 'Default example string',
age: 30
}
// Ideally, { firstName: string, lastName: string, age: number }
type ExampleType = typeof example;
But I want the return of my generate-example functionality to be typed automatically. Currently, it's just throwing its hands up and returning any
.
Its basic structure is that it has a processSchema
function that takes any schema type (whether object, oneOf, or simple integer type), and then recursively runs through it "processing" each child.
Full playground of code here, and the current WIP implementation below:
type PropertyType = "string" | "number" | "integer" | "object" | "boolean" | "array";
type Property =
| BooleanProperty
| NumberProperty
| IntegerProperty
| StringProperty
| ObjectProperty
| ArrayProperty;
interface OneOf {
oneOf: PropertyOrKeyword[];
}
interface AnyOf {
anyOf: PropertyOrKeyword[];
}
type Keyword = OneOf | AnyOf;
type PropertyOrKeyword = Property | Keyword;
type Properties = Record<string, PropertyOrKeyword>;
interface BaseProperty<T> {
type: PropertyType;
enum?: Array<T>;
example?: T;
description?: string;
}
interface BooleanProperty extends BaseProperty<boolean> {
type: "boolean";
}
interface NumberProperty extends BaseProperty<number> {
type: "number";
minimum?: number;
maximum?: number;
format?: "float";
}
interface IntegerProperty extends BaseProperty<number> {
type: "integer";
minimum?: number;
maximum?: number;
}
type StringFormats =
// OpenAPI built-in formats: https://swagger.io/docs/specification/data-models/data-types/#string
| "date"
| "date-time"
| "password"
| "byte"
| "binary"
// But arbitrary others are accepted
| "uuid"
| "email";
interface StringProperty extends BaseProperty<string> {
type: "string";
format?: StringFormats;
/** A string of a regex pattern **/
pattern?: string;
}
interface ObjectProperty extends BaseProperty<Record<string, Property>> {
type: "object";
properties: Record<string, PropertyOrKeyword>;
required?: string[];
title?: string; // If a schema
additionalProperties?: boolean;
}
interface ArrayProperty extends BaseProperty<Array<any>> {
type: "array";
items: PropertyOrKeyword;
}
class Example {
example;
schema: PropertyOrKeyword;
constructor(schema: PropertyOrKeyword) {
this.schema = schema;
const value = this._processSchema(schema);
this.example = value as typeof value;
}
fullExample(description?: string, externalValue?: string) {
return { value: this.example, description, externalValue };
}
/** Traverses schema and builds an example object from its properties */
_processSchema(schema: PropertyOrKeyword) {
if ("oneOf" in schema) {
return this._processSchema(schema.oneOf[0]);
} else if ("anyOf" in schema) {
return this._processSchema(schema.anyOf[0]);
} else if ("items" in schema) {
return [this._processSchema(schema.items)];
} else if ("type" in schema) {
if (schema.type === "object") {
return Object.entries(schema.properties).reduce(
(obj, [key, val]) => ({
[key]: this._processSchema(val as PropertyOrKeyword),
...obj,
}),
{} as object
);
} else {
if (["integer", "number"].includes(schema.type)) this._processSimpleProperty(schema) as number;
if (schema.type === "boolean") this._processSimpleProperty(schema) as boolean;
if (schema.type === "number") this._processSimpleProperty(schema) as number;
return this._processSimpleProperty(schema) as string;
}
}
}
/** Produces a sensible example for non-object properties */
_processSimpleProperty(
prop: NumberProperty | StringProperty | BooleanProperty | IntegerProperty
): number | boolean | string {
// If an example has been explicitly set, return that
if (prop.example) return prop.example;
// If an enum type, grab the first option as an example
if (prop.enum) return prop.enum[0];
// If a string type with format, return a formatted string
if (prop.type === "string" && prop.format) {
return {
uuid: "asdfa-sdfea-wor13-dscas",
date: "1970-01-14",
["date-time"]: "1970-01-14T05:34:58Z+01:00",
email: "[email protected]",
password: "s00pers33cret",
byte: "0d5b4d43dbf25c433a455d4e736684570e78950d",
binary: "01101001001010100111010100100110100d",
}[prop.format] as string;
}
// Otherwise, return a sensible default
return {
string: "Example string",
integer: 5,
number: 4.5,
boolean: false,
}[prop.type];
}
}
const spec: ObjectProperty = {
type: 'object',
properties: {
firstName: {
type: 'string',
example: 'Johnbob',
},
lastName: {
type: 'string',
// No example provided
},
age: {
type: 'integer',
example: 30
},
}
};
const spec: ObjectProperty = {
type: 'object',
properties: {
firstName: {
type: 'string',
example: 'Johnbob',
},
lastName: {
type: 'string',
// No example provided
},
age: {
type: 'integer',
example: 30
},
favoriteThing: {
oneOf: [{
type: 'object',
properties: {
name: {
type: 'string',
example: 'Beer'
},
liters: {
type: 'integer',
example: 1
}
}
},
{
type: 'object',
properties: {
name: {
type: 'string',
example: 'Movie'
},
lengthInMins: {
type: 'integer',
example: 120
}
}
}
]
}
}
};
console.log(new Example(spec).example)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
这里的主要目标是编写类型函数
类型schematotype&lt; t&gt; = ...
将架构类型作为输入,并产生相应的值类型作为输出。因此,您想要schematotype&lt; {type:“ string”}&gt;
bestring
,以及schematotype&lt; {type:“ array”,tyme:type:type:type:type: “ number”}}}&gt;
benumber []
等。首先,我们可能应该写一个
schema
键入该键模式类型可分配给。这不是严格必要的,如果愿意,您可以做不同的事情,但是这是一种方法:当然,您可以使用其他模式类型来增强此定义。您可以看到
架构
是联合特定架构类型的类型,其中一些(Objectschema
,arrayschema
,单一
和anyof
)是按schema
本身递归定义的。我为什么要允许 /code>数组是因为它比常规读取阵列的限制类型较小,并且因为 /a>倾向于导致可读的元组类型。
因此,这是编写
schematotype&lt; t&gt;
的一种方法:这是递归条件类型。这个想法是,我们浏览每个
schema
联合成员并将其转换为相应的值类型。好吧,我们要做的第一件事是架构扩展t?未知:
,这意味着如果t
正是schema
本身,我们将缩短计算并返回 the未知
类型。这种事情对于避免循环警告是必要的,因为否则schematotype&lt; schema&gt;
最终将永远递归自己。让我们看一下该类型的其他行:
如果
t
是Oneof
,那么我们需要 indexOneof
属性,该属性将是模式的数组...因此,我们将其进一步索引number
valued属性(提供数组元素类型)。t ['Oneof'] [number]
将是数组中模式类型的结合。而且由于schematotype
是(主要是)a 分配条件类型,然后schematotype&lt; t ['Oneof'] [number]&gt;
本身将是该联合每个元素的价值类型的结合。所有这些都意味着输出类型将是所有Oneof
数组元素值类型的联合。如果
t
是anyof
,我们与Oneof
做同样的事情。大概您的架构在乎“任何”和“一个”和“一个”之间的区别,但是在打字稿中,这两者都以联盟为代表。如果您真的很关心差异,则可以使用 Oneof 构建一个“独家”联盟, 2887218“>这个问题,但我认为这是出于范围。这是简单的...我写了
primmapping
是来自type
名称到原始类型的地图。因此,如果t
是{type:“ integer”}
,则值类型为primmapping [“ integer”]
,它被证明为号码
。如果
t
是objectschema
,那么我们将映射类型从properties
属性上的每个键上的类型t
。最后:如果
t
是arrayschema
,那么我们将与items
属性的类型进行数组>。让我们测试一下:
看起来不错!
当您实现某些函数或类方法,该函数或类方法是类型
schema
并产生示例时,您可能会遇到一些问题,使编译器可以推断出架构的正确类型。如果将上述spec
initializer不用作为const ,则编译器甚至不会记住特定的字面类型类型
属性和事物中断:因此,如果将事物保存到他们自己的变量中,则需要或类似的东西。
您可以以使编译器尝试从其输入中推断出狭窄类型的方式编写功能/方法,但这有点令人讨厌。请参阅 Microsoft/typeScript#30680 对于功能请求,可以更轻松地使其更容易。这是实现它的一种方法:
所有
schemahelper
确实是要求编译器注意type
字面类型和单个数组类型和值。但是Yuck。无论如何,让我们测试一下:
我认为看起来不错。输出类型是您想要的。
The main goal here is to write a type function
type SchemaToType<T> = ...
which takes a schema type as input and produces the corresponding value type as output. So you wantSchemaToType<{type: "string"}>
to bestring
, andSchemaToType<{type: "array", items: {type: "number"}}>
to benumber[]
, etc.First we should probably write a
Schema
type that all schema types are assignable to. This isn't strictly necessary, and you can do it differently if you want, but here's one way to do it:And of course you can augment this definition with other schema types. You can see that
Schema
is a union of specific schema types, some of which (ObjectSchema
,ArraySchema
,OneOf
, andAnyOf
) are recursively defined in terms ofSchema
itself.The reason why I want to allow
readonly
arrays is because it is a less restrictive type than regular read-write arrays, and becauseconst
assertions tend to result in readonly tuple types.Armed with that, here's one way to write
SchemaToType<T>
:This is a recursive conditional type. The idea is that we go through each
Schema
union member and convert it into the corresponding value type. Well, the first thing we do isSchema extends T ? unknown :
, which means that ifT
turns out to be justSchema
itself, we short-circuit the computation and return theunknown
type. This sort of thing is necessary to avoid circularity warnings, since otherwiseSchemaToType<Schema>
would end up recursing into itself forever.Let's look at the other lines of that type:
If
T
is aOneOf
, then we need to index into itsoneOf
property, which is going to be an array of schemas... so we index further into itsnumber
-valued properties (which gives the array element type).T['oneOf'][number]
will be a union of the schema types in the array. And sinceSchemaToType
is (mostly) a distributive conditional type, thenSchemaToType<T['oneOf'][number]>
will itself be a union of the value types for each element of that union. All of this means that the output type will be a union of all theoneOf
array element value types.If
T
isAnyOf
, we do the same thing asOneOf
. Presumably your schema cares about the difference between "any of" and "one of", but in TypeScript both of these are fairly well represented by a union. If you really care about the difference, you can build an "exclusive" union forOneOf
, using a technique like that shown in this question, but I consider that out of scope here.This is the easy one... I wrote
PrimMapping
to be a map fromtype
names to primitive types. So ifT
is{type: "integer"}
, then the value type isPrimMapping["integer"]
, which turns out to benumber
.If
T
is anObjectSchema
then we make a mapped type over each key from theproperties
property ofT
. And finally:If
T
is anArraySchema
then we make an array of the type corresponding to theitems
property ofT
.Let's test it out:
Looks good!
When you come to implement some function or class method that takes a value of type
Schema
and produces an example, you might have some issues getting the compiler to infer the right type for the schema. If you write the abovespec
initializer without theas const
, the compiler will fail to even remember the specific literal types of thetype
property and things break:So if you save things to their own variables you need
as const
or something like it.You can write the function/method in such a way as to make the compiler try to infer narrow types from its input, but it's a bit obnoxious. See microsoft/TypeScript#30680 for a feature request to make it easier. Here's one way to implement it:
All
SchemaHelper
does is ask the compiler to pay attention to thetype
literal types and the individual array types and values. But yuck.In any case, let's test it out:
Looks good, I think. The output types are the ones you wanted.
Playground link to code