如何为TypeScript属性创建@Relationship装饰器并解决反序列化问题
Let's tackle your two TypeScript challenges one by one with practical, hands-on solutions that fit your constraints:
@Relationship Decorator for TypeScript Properties TypeScript property decorators let you attach metadata to class properties—perfect for marking ORM-style relationships. Here's a step-by-step implementation:
Step 1: Set up the decorator with metadata storage
We'll usereflect-metadata(install it vianpm install reflect-metadata, and enableexperimentalDecorators+emitDecoratorMetadatain yourtsconfig.json) to store relationship details.import 'reflect-metadata'; // Unique key to identify relationship metadata const RELATIONSHIP_KEY = Symbol('relationship'); // Decorator function (supports lazy model references to avoid circular imports) export function Relationship<T>(targetModel?: () => new () => T) { return (target: any, propertyKey: string | symbol) => { Reflect.defineMetadata( RELATIONSHIP_KEY, { targetModel }, target, propertyKey ); }; }Step 2: Mark properties with the decorator
Use@Relationshipto flag properties that represent related models:class User { id: number; name: string; @Relationship(() => Post) // Lazy reference to Post posts: Post[]; } class Post { id: number; title: string; @Relationship(() => User) // Lazy reference to User author: User; }Step 3: Retrieve metadata for processing
When you need to handle relationships (like during deserialization), fetch the stored metadata:function getRelationshipMeta(target: any, prop: string) { return Reflect.getMetadata(RELATIONSHIP_KEY, target, prop); }
Result in a Generic Model Class Without a deserialize Method Since you don't want to explicitly declare a deserialize method, we can leverage the base class constructor + our @Relationship metadata to auto-initialize the Result property when instantiating from JSON. Here's how:
Step 1: Build a generic base
Modelclass
This base class will handle property copying and relationship initialization automatically in its constructor:export abstract class Model<T> { constructor(data: Partial<T>) { // Copy basic enumerable properties first (matches your original deserialize behavior) Object.assign(this, data); // Process all relationship properties const prototype = Object.getPrototypeOf(this); const propNames = Object.getOwnPropertyNames(prototype); for (const prop of propNames) { const meta = getRelationshipMeta(prototype, prop); if (!meta?.targetModel) continue; const targetModel = meta.targetModel(); const rawValue = (data as any)[prop]; if (Array.isArray(rawValue)) { // Initialize array relationships (e.g., User.posts) (this as any)[prop] = rawValue.map(item => new targetModel(item)); } else if (rawValue) { // Initialize single-object relationships (e.g., your Body.result) (this as any)[prop] = new targetModel(rawValue); } } } }Step 2: Extend the base class for your models
YourBodyandResultclasses just need to extendModel, and theResultproperty will auto-initialize when you create aBodyinstance from JSON:class Result extends Model<Result> { id: string; value: number; } class Body extends Model<Body> { id: number; name: string; @Relationship(() => Result) result: Result; } // Usage: Instantiate Body from JSON—Result is automatically initialized const jsonInput = { id: 1, name: "Sample", result: { id: "res-001", value: 42 } }; const bodyInstance = new Body(jsonInput); console.log(bodyInstance.result instanceof Result); // Logs true console.log(bodyInstance.result.value); // Logs 42Key benefits:
- No separate
deserializemethod needed—the constructor handles all initialization logic. - Works with generic
Modelclasses since we rely on runtime metadata rather than hardcoded type checks. - Handles both single-object and array relationships out of the box.
- No separate
内容的提问来源于stack exchange,提问作者Filipe

