Why Runtime Type Checking Matters in TypeScript and How to Implement It

The article explains the difference between compile‑time and runtime type checking, why runtime checks are essential for data‑exchange scenarios in TypeScript projects, and compares several practical solutions—including JSON schemas, API‑based libraries like Zod, class‑validator, TypeScript‑JSON‑Schema, TypeScript‑is, and DeepKit—highlighting their advantages, drawbacks, and implementation details.

Watermelon Frontend Tech Team
Watermelon Frontend Tech Team
Watermelon Frontend Tech Team
Why Runtime Type Checking Matters in TypeScript and How to Implement It

What is Runtime Type Checking?

Compile‑time type checking (static): Types are verified during compilation; the emitted code contains no type annotations and does not affect runtime behavior.

Runtime type checking (dynamic): Types are validated while the program runs, typically to guard data passed between functions, APIs, or external services.

Why Do We Need Runtime Type Checking?

TypeScript greatly improves front‑end maintainability, but when exchanging data with external sources, compile‑time checks alone can miss issues. Two real incidents illustrate the problem:

Internal input: an API changed a video ID field from string to number, causing precision loss on the front end.

External output: a logic change removed a tracking field, which went unnoticed for a long time, wasting analysis effort.

Additional scenarios that benefit from runtime checks include form validation, API/JSB testing, and filtering sensitive fields before reporting.

How to Perform Runtime Type Checking?

interface MyDataType {
    video_id: string;
    user_info: {
        user_id: number;
        email: string;
    };
    image_list: { url: string }[];
}

const data: MyDataType = await fetchMyData();

if (
    typeof data.video_id === 'string' &&
    data.user_info &&
    typeof data.user_info.user_id === 'number' &&
    typeof data.user_info.email === 'string' &&
    Array.isArray(data.image_list) &&
    data.image_list.every(image => typeof image.url === 'string')
) {
    // do something
}

Manually writing such checks is error‑prone and requires maintaining two parallel type definitions. Below are several industry‑standard solutions that aim to keep a single source of truth for both static and runtime validation.

Solution 1 – JSON‑Based Schemas

Write runtime validation rules in JSON and extract static TypeScript types from them.

JSON Example (AJV)

import Ajv, { JTDDataType } from "ajv/dist/jtd";

const ajv = new Ajv();
const schema = {
    properties: {
        video_id: { type: "string" },
        user_info: {
            properties: {
                user_id: { type: "int32" },
                email: { type: "string" }
            }
        },
        image_list: {
            elements: {
                properties: { url: { type: "string" } }
            }
        }
    }
} as const;

type MyDataType = JTDDataType<typeof schema>;

const data: MyDataType = await fetchMyData();
const validate = ajv.compile(schema);
validate(data);
if (validate.errors) {
    // handle errors
}

Pros:

JTD can generate TypeScript types from the schema, avoiding duplicate definitions.

Validation libraries provide advanced rules (date ranges, email formats, etc.).

JSON is easy to store, transmit, and can be used across languages.

Cons:

Schema syntax has a learning curve and can be verbose.

Implementation: The validator iterates over the schema rules to compare data fields, and TypeScript types are extracted via conditional types and inference.

API‑Based Schemas

Libraries such as zod, superstruct, and io‑ts let you compose validation rules with a fluent API.

import { z } from "zod";

const schema = z.object({
    video_id: z.string(),
    user_info: z.object({
        user_id: z.number().positive(),
        email: z.string().email()
    }),
    image_list: z.array(z.object({ url: z.string() }))
});

type MyDataType = z.infer<typeof schema>;

const data: MyDataType = await fetchMyData();
const parseRes = schema.safeParse(data);
if (parseRes.error) {
    // handle errors
}

Pros:

More concise and readable than raw JSON.

Provides advanced validation rules.

Can infer TypeScript types directly.

Cons:

Requires learning a new API; not pure TypeScript syntax.

Implementation: Similar to the JSON approach but with a lighter runtime footprint (e.g., zod ~10 KB vs. ajv ~35 KB).

Solution 2 – Class‑Validator + Decorators

Combine static TypeScript types with runtime checks using class decorators.
import 'reflect-metadata';
import { plainToClass, Type } from "class-transformer";
import {
    validate,
    IsString,
    IsInt,
    IsEmail,
    IsObject,
    IsArray,
    ValidateNested,
    IsNotEmpty
} from "class-validator";

class UserInfo {
    @IsInt()
    user_id: number;
    @IsEmail()
    email: string;
}

class LargeImage {
    @IsString()
    url: string;
}

class MyData {
    @IsString()
    @IsNotEmpty({ message: 'video_id cannot be empty' })
    video_id: string;

    @IsObject()
    @ValidateNested()
    @Type(() => UserInfo)
    user_info: UserInfo;

    @IsArray({ message: 'array cannot be empty' })
    @ValidateNested({ each: true })
    @Type(() => LargeImage)
    image_list: LargeImage[];
}

const data: MyData = await fetchMyData();
const dataAsClassInstance = plainToClass(MyData, data);
validate(dataAsClassInstance).then(messages => {
    // handle validation messages
});

Pros:

Enforces atomic types; suitable for server‑side ORM‑style validation.

Customizable error messages.

Can extract TypeScript types from the class definitions.

Cons:

Both static and runtime definitions must be written, though they coexist in the same file.

Nested objects require additional boilerplate.

Works primarily with class instances; plain objects need transformation.

Implementation: Decorators attach metadata to class fields; at runtime, reflection reads this metadata to perform validation.

Solution 3 – Generate Runtime Checks from TypeScript Types

Transform TypeScript types into JSON schemas or runtime validators automatically.

TypeScript‑JSON‑Schema

Generates JSON schemas from existing TypeScript interfaces, which can then be fed to any JSON‑schema validator.

Pros: No need to maintain duplicate type definitions.

Cons: Does not perform validation itself; requires an additional validator and may not support all TypeScript features (e.g., unions).

typescript‑is (Compile‑time Code Generation)

import { is } from "typescript-is";

interface MyDataType {
    gid: number;
    user_info: { user_id: number; email: string };
    large_image_list: { url: string }[];
}

const data: MyDataType = fetchMyData();
const isRightType = is<MyDataType>(data);

Pros: Generates runtime checks directly from TypeScript types without extra schema files.

Cons: Can inflate bundle size because the generated validation code is large; lacks some advanced validation rules.

Implementation: A TypeScript transformer plugin rewrites the AST to emit validation functions, similar to Babel plugins.

DeepKit – Runtime Type Information

Compiles TypeScript type metadata into a lightweight bytecode that can be interpreted at runtime, enabling validation, mock data generation, and more.

import { is } from '@deepkit/type';

interface MyDataType {
    video_id: string;
    user_info: { user_id: number; email: string };
    image_list: { url: string }[];
}

const data: MyDataType = await fetchMyData();
const isRightType = is<MyDataType>(data);

Pros: Small runtime footprint, supports advanced rules, and keeps full type information at runtime.

Cons: Still experimental, with uncertain stability and potential performance overhead.

Implementation: TypeScript types are compiled into bytecode; a runtime interpreter reads this bytecode to perform reflection‑based checks.

Conclusion

No single solution is perfect. For most projects, API‑based libraries like zod offer a mature, flexible, and easy‑to‑use approach. Emerging tools such as DeepKit show promise for broader runtime type‑driven capabilities, but they are not yet widely adopted.

TypeScript itself is unlikely to add native runtime type information, as the design goals explicitly avoid emitting runtime metadata.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

TypeScriptJSON SchemaValidationzodruntime type checking
Watermelon Frontend Tech Team
Written by

Watermelon Frontend Tech Team

We are from ByteDance, the frontend division of Watermelon Video, responsible for its product development. We share business practices from the product to provide valuable experience to the industry, covering areas such as marketing setups, interactive features, engineering capabilities, stability, Node.js, and middle‑back office development.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.