This is how you generate TypeScript types from JSON Schema definitions, and why it’s way more powerful than you think.

Imagine you’re building an API that sends and receives JSON data. You write a JSON Schema to describe the shape of that data. Now, instead of manually writing TypeScript types to match that schema, you can generate them automatically. This keeps your types in sync with your data contracts, preventing runtime errors and making your code more robust.

Let’s see it in action. We’ll use quicktype, a fantastic tool that can generate code from JSON, JSON Schema, and more.

First, install it:

npm install -g quicktype

Now, create a simple JSON Schema. Let’s say we’re defining a User object:

{
  "type": "object",
  "properties": {
    "id": {
      "type": "integer",
      "description": "Unique identifier for the user"
    },
    "username": {
      "type": "string",
      "description": "The user's chosen username"
    },
    "email": {
      "type": "string",
      "format": "email",
      "description": "The user's email address"
    },
    "isActive": {
      "type": "boolean",
      "default": true,
      "description": "Indicates if the user account is active"
    }
  },
  "required": [
    "id",
    "username",
    "email"
  ]
}

Save this as userSchema.json.

Now, run quicktype to generate TypeScript:

quicktype userSchema.json -o userTypes.ts

This will create a userTypes.ts file with the following content:

// This file is generated by quicktype. DO NOT EDIT.

export interface User {
  id: number;
  username: string;
  email: string;
  isActive?: boolean;
}

That’s the basic usage. quicktype understands standard JSON Schema types (string, number, integer, boolean, array, object) and maps them directly to TypeScript equivalents. It also handles required fields by making them non-optional in the generated type.

But quicktype goes deeper. It can interpret description fields as JSDoc comments and format attributes for more precise typing.

Let’s add a more complex schema, perhaps an array of Product objects, each with nested properties and an enum:

{
  "type": "array",
  "items": {
    "type": "object",
    "properties": {
      "productId": {
        "type": "string",
        "description": "Unique identifier for the product"
      },
      "name": {
        "type": "string"
      },
      "price": {
        "type": "number",
        "format": "float",
        "exclusiveMinimum": 0
      },
      "category": {
        "type": "string",
        "enum": ["Electronics", "Books", "Clothing", "Home"]
      },
      "details": {
        "type": "object",
        "properties": {
          "weightKg": {
            "type": "number",
            "description": "Weight in kilograms"
          },
          "dimensionsCm": {
            "type": "object",
            "properties": {
              "length": {"type": "number"},
              "width": {"type": "number"},
              "height": {"type": "number"}
            },
            "required": ["length", "width", "height"]
          }
        },
        "required": ["weightKg"]
      }
    },
    "required": [
      "productId",
      "name",
      "price",
      "category"
    ]
  }
}

Save this as productsSchema.json.

Now, generate types for this:

quicktype productsSchema.json -o productsTypes.ts

The generated productsTypes.ts will look something like this:

// This file is generated by quicktype. DO NOT EDIT.

export type Category = "Electronics" | "Books" | "Clothing" | "Home";

export interface Product {
  productId: string;
  name: string;
  price: number;
  category: Category;
  details: Details;
}

export interface Details {
  weightKg: number;
  dimensionsCm: DimensionsCm;
}

export interface DimensionsCm {
  length: number;
  width: number;
  height: number;
}

Notice how quicktype inferred the Category type as a union of the enum values and created separate interfaces for nested objects like Details and DimensionsCm. It also correctly interpreted format: "float" for the price as a number. The exclusiveMinimum: 0 constraint isn’t directly translated into a TypeScript type but is a valuable piece of metadata for validation.

The real magic happens when you combine this with your actual data. Suppose you have an incoming JSON payload that you expect to conform to your User schema. You can parse it and then assert its type:

import { User } from './userTypes'; // Assuming userTypes.ts is generated

const rawUserData = `{
  "id": 123,
  "username": "alice",
  "email": "alice@example.com",
  "isActive": false
}`;

try {
  const userData: User = JSON.parse(rawUserData);
  console.log(`User ID: ${userData.id}, Username: ${userData.username}`);
  // Now you have type safety for all properties of userData
} catch (error) {
  console.error("Failed to parse user data:", error);
}

This doesn’t perform runtime validation against the schema, but it does give you compile-time safety. If you try to access userData.emailAddress (a typo), TypeScript will immediately flag it.

This approach is incredibly useful for:

  • API Clients/Servers: Ensuring request/response payloads match defined contracts.
  • Configuration Files: Generating types for complex .json config files.
  • Data Transformation: Validating and typing data being moved between systems.

The one thing most people don’t realize is that quicktype can also infer JSON Schema from example JSON data. If you have a sample of your data, you can generate the schema and then generate types from that schema, effectively creating a schema and types from scratch.

echo '{"name": "Example", "value": 100}' > example.json
quicktype example.json --just-types --lang ts --schema-output schema.json
# This generates schema.json and then uses it to create types.

The next frontier is integrating this with runtime validation libraries like ajv to ensure that your parsed JSON actually conforms to the schema at runtime, not just at compile time.

Want structured learning?

Take the full Typescript course →