You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何验证JS对象与TypeScript类属性匹配并实现双阶段校验?

Validate Normalizr Output Against TypeScript Classes (Single Type Definition)

Got it, let's solve this problem: you're using normalizr which returns untyped objects, casting with as feels risky, and you want to define your types once to get both TypeScript compile-time checks and runtime validation in unit tests—including support for nested class structures.

First, Let's Recap Your Existing Setup

You've got these TypeScript classes (your single source of truth):

class User {
  id: number;
}

class Entities {
  users: { [key: string]: User };
}

class MyClass {
  entities: Entities;
}

And a normalizr output object that looks like this:

const myLiteral = { 
  entities: { 
    users: { 
      '1': { id: 1 }, 
      '2': { id: 2 } 
    } 
  } 
};

Approach 1: Use Class-Based Runtime Validation (No Extra Libraries)

Since TypeScript classes exist at runtime (unlike interfaces), we can build a reusable validator that checks if an object matches your class structure—including nested classes and index signatures.

Step 1: Enable Decorator Metadata

First, add emitDecoratorMetadata: true to your tsconfig.json, then install the reflect-metadata package:

npm install reflect-metadata

Import it at the top of your test file or app entry point:

import 'reflect-metadata';

Step 2: Build the Generic Validator Function

This function recursively checks if an object matches a class's structure:

function matchesClass<T>(obj: unknown, cls: new () => T): obj is T {
  // Basic sanity check: obj is an object and not null
  if (typeof obj !== 'object' || obj === null) return false;

  // Get all class properties (skip the constructor)
  const classProperties = Object.getOwnPropertyNames(cls.prototype)
    .filter(prop => prop !== 'constructor');

  for (const prop of classProperties) {
    // Check if the property exists on the object
    if (!(prop in obj)) return false;

    const objValue = (obj as Record<string, unknown>)[prop];
    // Get the expected property type from class metadata
    const expectedType = Reflect.getMetadata('design:type', cls.prototype, prop);

    // Handle nested classes (e.g., MyClass.entities → Entities)
    if (typeof expectedType === 'function' && expectedType.prototype instanceof Object) {
      if (!matchesClass(objValue, expectedType)) return false;
    }
    // Handle index signatures (e.g., Entities.users → { [key: string]: User })
    else if (expectedType === Object && prop === 'users' && cls === Entities) {
      const userRecords = objValue as Record<string, unknown>;
      for (const user of Object.values(userRecords)) {
        if (!matchesClass(user, User)) return false;
      }
    }
    // Handle primitive types (add more checks for strings/booleans if needed)
    else if (expectedType === Number && typeof objValue !== 'number') {
      return false;
    }
  }

  return true;
}

Step 3: Write Your Unit Test

Use the validator to confirm your normalizr output matches the class structure:

import { expect } from 'vitest'; // or Jest, Mocha, etc.

it('normalizr output matches MyClass structure', () => {
  const myLiteral = { 
    entities: { 
      users: { 
        '1': { id: 1 }, 
        '2': { id: 2 } 
      } 
    } 
  };

  const isCompatible = matchesClass(myLiteral, MyClass);
  expect(isCompatible).toBe(true);
});

Approach 2: Use a Schema Library (More Robust, Less Manual Work)

If you want to avoid maintaining reflection code, use a library like zod that lets you define a schema once for both runtime validation and TypeScript types.

Step 1: Install Zod

npm install zod

Step 2: Define Your Schema (Single Source of Truth)

import { z } from 'zod';

// Define the schema
const UserSchema = z.object({ id: z.number() });
const EntitiesSchema = z.object({ users: z.record(UserSchema) });
const MyClassSchema = z.object({ entities: EntitiesSchema });

// Infer the TypeScript type directly from the schema
type MyClass = z.infer<typeof MyClassSchema>;

Step 3: Test with Zod's Validation

it('normalizr output matches the schema', () => {
  const myLiteral = { 
    entities: { 
      users: { 
        '1': { id: 1 }, 
        '2': { id: 2 } 
      } 
    } 
  };

  const validationResult = MyClassSchema.safeParse(myLiteral);
  expect(validationResult.success).toBe(true);
});

This approach automatically handles edge cases like optional properties, unions, and arrays, with zero manual reflection code.


Key Benefits of These Solutions

  • Single Source of Truth: Either your classes or Zod schema act as the single definition for both compile-time checks and runtime validation
  • Nested Structure Support: Both solutions handle nested objects, classes, and index signatures (like your users record)
  • Catch Errors Early: TypeScript catches issues during development, and runtime tests ensure normalizr doesn't output unexpected structures

内容的提问来源于stack exchange,提问作者arhnee

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 07:20:14