如何验证JS对象与TypeScript类属性匹配并实现双阶段校验?
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
usersrecord) - Catch Errors Early: TypeScript catches issues during development, and runtime tests ensure normalizr doesn't output unexpected structures
内容的提问来源于stack exchange,提问作者arhnee

