Stop typing twice 🙅♂️
A lot of projects use JSON schemas for runtime data validation along with TypeScript for static type checking.
Their code may look like this:
const dogSchema = {
type: "object",
properties: {
name: { type: "string" },
age: { type: "integer" },
hobbies: { type: "array", items: { type: "string" } },
favoriteFood: { enum: ["pizza", "taco", "fries"] },
},
required: ["name", "age"],
};
type Dog = {
name: string;
age: number;
hobbies?: string[];
favoriteFood?: "pizza" | "taco" | "fries";
};
Both objects carry similar if not exactly the same information. This is a code duplication that can annoy developers and introduce bugs if not properly maintained.
That's when json-schema-to-ts
comes to the rescue 💪
FromSchema
The FromSchema
method allows infering TS types directly from JSON schemas:
import { FromSchema } from "json-schema-to-ts";
const dogSchema = {
type: "object",
properties: {
name: { type: "string" },
age: { type: "integer" },
hobbies: { type: "array", items: { type: "string" } },
favoriteFood: { enum: ["pizza", "taco", "fries"] },
},
required: ["name", "age"],
} as const;
type Dog = FromSchema<typeof dogSchema>;
Schemas can even be nested, as long as you don't forget the as const
statement:
const catSchema = {
type: "object",
properties: {
name: { type: "string" },
age: { type: "integer" },
favoriteThings: { enum: ["playing", "sleeping", "moreSleeping"] },
},
required: ["name", "age"],
} as const;
const petSchema = {
anyOf: [dogSchema, catSchema],
} as const;
type Pet = FromSchema<typeof petSchema>;
Note: The as const
statement is used so that TypeScript takes the schema definition to the word (e.g. true is interpreted as the true constant and not widened as boolean). It is pure TypeScript and has zero impact on the compiled code.
Docs
Installation
npm install --save-dev json-schema-to-ts
yarn add --dev json-schema-to-ts
Use cases
Const
const fooSchema = {
const: "foo",
} as const;
type Foo = FromSchema<typeof fooSchema>;
Enums
const enumSchema = {
enum: [true, 42, { foo: "bar" }],
} as const;
type Enum = FromSchema<typeof enumSchema>;
You can also go full circle with typescript enums
.
enum Food {
Pizza = "pizza",
Taco = "taco",
Fries = "Fries",
}
const enumSchema = {
enum: Object.values(Food),
} as const;
type Enum = FromSchema<typeof enumSchema>;
Litterals
const litteralSchema = {
type: "null",
} as const;
type Litteral = FromSchema<typeof litteralSchema>;
const litteralsSchema = {
type: ["null", "string"],
} as const;
type Litterals = FromSchema<typeof litteralsSchema>;
(For object
and array
types, properties like required
or additionalItems
will work 🙌)
Arrays
const arraySchema = {
type: "array",
items: { type: "string" },
} as const;
type Array = FromSchema<typeof arraySchema>;
Tuples
const tupleSchema = {
type: "array",
items: [{ type: "boolean" }, { type: "string" }],
} as const;
type Tuple = FromSchema<typeof tupleSchema>;
FromSchema
supports the additionalItems
specifications:
const tupleSchema = {
type: "array",
items: [{ type: "boolean" }, { type: "string" }],
additionalItems: false,
} as const;
type Tuple = FromSchema<typeof tupleSchema>;
const tupleSchema = {
type: "array",
items: [{ type: "boolean" }, { type: "string" }],
additionalItems: { type: "number" },
} as const;
type Tuple = FromSchema<typeof tupleSchema>;
...as well as the minItems
and maxItems
specifications:
const tupleSchema = {
type: "array",
items: [{ type: "boolean" }, { type: "string" }],
minItems: 1,
maxItems: 2,
} as const;
type Tuple = FromSchema<typeof tupleSchema>;
(NOTE: Additional items will only work if Typescript's strictNullChecks
option is activated)
Objects
const objectSchema = {
type: "object",
properties: {
foo: { type: "string" },
bar: { type: "number" },
},
required: ["foo"],
} as const;
type Object = FromSchema<typeof objectSchema>;
FromSchema
partially supports the additionalProperties
and patternProperties
specifications:
additionalProperties
can be used to deny additional items.
const closedObjectSchema = {
...objectSchema,
additionalProperties: false,
} as const;
type Object = FromSchema<typeof additionalPropertiesSchema>;
- Used on their own,
additionalProperties
and/or patternProperties
can be used to type unnamed properties.
const objectSchema = {
type: "object",
additionalProperties: {
type: "boolean",
},
patternProperties: {
"^S": { type: "string" },
"^I": { type: "integer" },
},
} as const;
type Object = FromSchema<typeof typedValuesSchema>;
- However, when used in combination with the
properties
keyword, extra properties will always be typed as unknown
to avoid conflicts.
AnyOf
const anyOfSchema = {
anyOf: [
{ type: "string" },
{
type: "array",
items: { type: "string" },
},
],
} as const;
type AnyOf = FromSchema<typeof fooSchema>;
FromSchema
will correctly infer factored schemas:
const factoredSchema = {
type: "object",
properties: {
bool: { type: "boolean" },
},
required: ["bool"],
anyOf: [
{
properties: {
str: { type: "string" },
},
required: ["str"],
},
{
properties: {
num: { type: "number" },
},
},
],
} as const;
type Factored = FromSchema<typeof factoredSchema>;
OneOf
...Coming soon 😃
AllOf
...Coming soon 😃
If/Else
...Coming soon 😃