DerivedTypes Serialization (Frontend)
The DerivedTypes serialization system in the TypeScript frontend provides seamless polymorphic serialization that works hand-in-hand with the backend .NET implementation. This system allows you to deserialize JSON objects into their correct TypeScript class instances based on type identifiers.
Note: This documentation focuses on derived types and polymorphic serialization. For comprehensive information about the
@fielddecorator, including runtime type safety benefits and detailed usage patterns, see Field Decorator Documentation.
Overview
Section titled “Overview”The frontend DerivedTypes system consists of:
@derivedTypedecorator: Marks classes with unique identifiers matching the backendDerivedTypeclass: Manages metadata for derived type identifiersJsonSerializer: Handles serialization/deserialization with type information@fielddecorator: Defines fields with optional derivative type supportFieldclass: Represents field metadata including derivative type information
Key Concepts
Section titled “Key Concepts”Type Metadata System
Section titled “Type Metadata System”The frontend uses TypeScript decorators and reflect-metadata to store type information:
- Derived Type IDs: Unique string identifiers that must match exactly with backend
[DerivedType]attributes - Field Metadata: Information about properties including their types and possible derivatives
- Runtime Type Resolution: Dynamic instantiation of correct classes during deserialization
Runtime Resolution Behavior
Section titled “Runtime Resolution Behavior”At runtime, the deserializer resolves derived types from two candidate sources:
field.derivativesprovided by@field(..., derivatives)- Runtime registrations from
DerivedType.getDerivedTypesFor(field.type)
The runtime registrations are populated by the @derivedType decorator by walking the prototype chain.
Because of this, class-based hierarchies (base class + subclasses) are discovered automatically.
For interface-shaped polymorphism, remember that TypeScript interfaces are erased at runtime.
In those cases, use explicit field.derivatives, or pass targetType to @derivedType so the
type is registered against a concrete runtime constructor.
Decorator-Based Configuration
Section titled “Decorator-Based Configuration”Unlike the backend’s attribute-based approach, the frontend uses decorators to mark classes and fields with metadata.
Uniqueness Requirements
Section titled “Uniqueness Requirements”Derived type IDs must be unique per interface to enable proper polymorphic deserialization. While you can use any string format, using descriptive identifiers is recommended for readability:
Good approach - Descriptive and unique:
@derivedType('credit-card')export class CreditCard implements IPaymentMethod { }
@derivedType('paypal')export class PayPal implements IPaymentMethod { }Alternative approach - Using GUIDs for maximum uniqueness:
@derivedType('550e8400-e29b-41d4-a716-446655440001')export class CreditCard implements IPaymentMethod { }
@derivedType('550e8400-e29b-41d4-a716-446655440002')export class PayPal implements IPaymentMethod { }Note on GUIDs: While GUIDs guarantee uniqueness, they sacrifice readability. If using GUIDs, ensure they are stored in shared constants or configuration to maintain consistency across your codebase and backend services.
1. Define Your Interface
Section titled “1. Define Your Interface”export interface IPaymentMethod { amount: number;}2. Create Derived Types
Section titled “2. Create Derived Types”import { derivedType } from '@cratis/fundamentals';import { field } from '@cratis/fundamentals';
@derivedType('credit-card')export class CreditCard implements IPaymentMethod { @field(Number) amount!: number;
@field(String) cardNumber!: string;
@field(String) expiryDate!: string;}
@derivedType('paypal')export class PayPal implements IPaymentMethod { @field(Number) amount!: number;
@field(String) email!: string;}3. Configure Parent Classes with Derivatives
Section titled “3. Configure Parent Classes with Derivatives”export class Order { @field(String) orderId!: string;
@field(Object, false, [CreditCard, PayPal]) paymentMethod!: IPaymentMethod;
@field(Object, true, [CreditCard, PayPal]) alternativePayments!: IPaymentMethod[];}
If `paymentMethod` is typed as a concrete base class (instead of interface-only shape), runtimeregistration from `@derivedType` can often resolve the correct subtype without an explicit derivatives list.Keeping the list is still valid and can be useful for clarity.Serialization and Deserialization
Section titled “Serialization and Deserialization”For detailed information about serialization and deserialization operations, including usage examples and API details, see the JsonSerializer Documentation.
When working with polymorphic types, the JsonSerializer automatically handles:
- Adding
_derivedTypeIdduring serialization based on the@derivedTypedecorator - Using
_derivedTypeIdto instantiate the correct derived type during deserialization
Field Decorator Integration
Section titled “Field Decorator Integration”The @field decorator is essential for defining serializable properties. For comprehensive documentation on the @field decorator including all parameters, usage patterns, and runtime type safety benefits, see Field Decorator Documentation.
Serialization Format
Section titled “Serialization Format”The frontend produces the same JSON format as the backend:
{ "paymentMethod": { "amount": 99.99, "cardNumber": "****-1234", "expiryDate": "12/25", "_derivedTypeId": "credit-card" }}The _derivedTypeId property is automatically:
- Added during serialization based on the class’s
@derivedTypedecorator - Used during deserialization to instantiate the correct class
Advanced Scenarios
Section titled “Advanced Scenarios”Complex Nested Objects
Section titled “Complex Nested Objects”export class ShoppingCart { @field(String) cartId!: string;
@field(Order, true) orders!: Order[];
@field(Object, false, [CreditCard, PayPal]) defaultPayment!: IPaymentMethod;}Mixed Collections
Section titled “Mixed Collections”export class PaymentHistory { @field(Object, true, [CreditCard, PayPal, BankTransfer]) transactions!: IPaymentMethod[];}Well-Known Types
Section titled “Well-Known Types”The system automatically handles common types:
@field(Date)createdAt!: Date;
@field(Guid)id!: Guid;
@field(Boolean)isActive!: boolean;Error Handling
Section titled “Error Handling”Common Issues
Section titled “Common Issues”- Missing
@derivedTypedecorator: Classes won’t be recognized during deserialization - Mismatched IDs: Frontend and backend derived type IDs must match exactly
- Missing field decorators: Properties without
@fieldwon’t be serialized/deserialized - Missing derivatives list: Polymorphic fields need the derivatives parameter
Debugging Tips
Section titled “Debugging Tips”// Check if a type has derived type metadataconst derivedTypeId = DerivedType.get(CreditCard);console.log(`CreditCard ID: ${derivedTypeId}`);
// Inspect field metadataconst fields = Fields.getFieldsForType(Order);console.log('Order fields:', fields);Best Practices
Section titled “Best Practices”1. Consistent Identifiers
Section titled “1. Consistent Identifiers”Ensure derived type IDs match exactly between frontend and backend:
// Frontend@derivedType('credit-card')export class CreditCard implements IPaymentMethod { }// Backend[DerivedType("credit-card")]public class CreditCard : IPaymentMethod { }2. Centralize Type Identifiers
Section titled “2. Centralize Type Identifiers”Store derived type IDs in a single location to avoid duplication and ensure consistency:
export const DERIVED_TYPE_IDS = { CREDIT_CARD: 'credit-card', PAYPAL: 'paypal', BANK_TRANSFER: 'bank-transfer',} as const;
// usage@derivedType(DERIVED_TYPE_IDS.CREDIT_CARD)export class CreditCard implements IPaymentMethod { }
@derivedType(DERIVED_TYPE_IDS.PAYPAL)export class PayPal implements IPaymentMethod { }Why centralize? These “magic strings” are critical for serialization consistency. Centralizing them in one place prevents duplicates, makes them easy to maintain, and ensures frontend and backend stay synchronized.
3. Complete Field Decoration
Section titled “3. Complete Field Decoration”Decorate all serializable properties:
export class CreditCard implements IPaymentMethod { @field(Number) amount!: number;
@field(String) cardNumber!: string;
// ❌ This won't be serialized without @field private internalId: string = '';
// ✅ Private fields that should be serialized need @field too @field(String) private securityCode!: string;}4. Explicit Derivative Lists
Section titled “4. Explicit Derivative Lists”Always specify derivatives for polymorphic fields:
// ✅ Good - explicit derivatives list@field(Object, false, [CreditCard, PayPal])paymentMethod!: IPaymentMethod;
// ❌ Bad - missing derivatives, won't deserialize correctly@field(Object)paymentMethod!: IPaymentMethod;5. Interface Consistency
Section titled “5. Interface Consistency”Keep interfaces synchronized between frontend and backend:
// Frontend interface should match backend interfaceexport interface IPaymentMethod { amount: number; // matches decimal Amount in C#}6. Testing
Section titled “6. Testing”Test serialization round-trips:
## Testing
For testing serialization round-trips with derived types, create tests that verify:
1. Polymorphic types deserialize to the correct class2. Derived type IDs are preserved during serialization3. Frontend IDs match backend IDs exactly
Refer to the [JsonSerializer Documentation](/fundamentals/typescript/serialization/json_serializer/) for comprehensive testing examples.Type System Integration
Section titled “Type System Integration”Working with TypeScript’s Type System
Section titled “Working with TypeScript’s Type System”// Type-safe polymorphic handlingfunction processPayment(payment: IPaymentMethod): void { if (payment instanceof CreditCard) { // TypeScript knows this is CreditCard console.log(`Processing card: ${payment.cardNumber}`); } else if (payment instanceof PayPal) { // TypeScript knows this is PayPal console.log(`Processing PayPal: ${payment.email}`); }}
// Type guards for runtime checkingfunction isCreditCard(payment: IPaymentMethod): payment is CreditCard { return payment instanceof CreditCard;}Generic Serialization
Section titled “Generic Serialization”// Generic deserializationfunction deserializePayments<T extends IPaymentMethod>( json: string, targetType: Constructor<T>): T[] { return JsonSerializer.deserializeArray(targetType, json);}
const creditCards = deserializePayments(json, CreditCard);Runtime Type Safety
Section titled “Runtime Type Safety”The @field decorator system provides significant runtime type safety benefits beyond TypeScript’s compile-time checking. For detailed information about runtime type safety, including comprehensive examples and comparisons, see the Field Decorator Documentation.
With derived types, you benefit from:
- Automatic polymorphic type resolution at runtime
- Correct class instantiation based on
_derivedTypeId - Full method access on all polymorphic instances
- Type-safe instanceof checks across derived types
Performance Considerations
Section titled “Performance Considerations”- Reflection overhead: Metadata lookup happens at runtime
- Type scanning: Keep derivative lists focused to avoid unnecessary type checking
- Memory usage: Metadata is stored per-class, not per-instance
Integration with Build Tools
Section titled “Integration with Build Tools”TypeScript Configuration
Section titled “TypeScript Configuration”Ensure your tsconfig.json includes:
{ "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true }}Bundling Considerations
Section titled “Bundling Considerations”When using module bundlers, ensure reflect-metadata is properly included:
import 'reflect-metadata';// Import this before any decorated classesSee Also
Section titled “See Also”- JsonSerializer - Core serialization utility for type-safe JSON conversion
- Field Decorator - Comprehensive guide to the
@fielddecorator system and runtime type safety