Skip to content

Latest commit

 

History

History
1513 lines (1178 loc) · 46.7 KB

File metadata and controls

1513 lines (1178 loc) · 46.7 KB

🚀 Date Interceptors

Production-ready, security-hardened date/time conversion for JSON APIs
Automatically converts ISO 8601 date strings in JSON responses into native Date objects — deeply, safely, and blazingly fast.

CodeFactor GitHub Actions Workflow Status Sonar Tests Sonar Coverage Quality Gate Status Bugs Code Smells Coverage Duplicated Lines (%) Lines of Code Reliability Rating Security Rating Maintainability Rating Technical Debt Vulnerabilities


📊 NPM Downloads

NPM Downloads @adaskothebeast/angular-date-http-interceptor NPM Downloads @adaskothebeast/angular-typed-http-client NPM Downloads @adaskothebeast/axios-interceptor NPM Downloads @adaskothebeast/hierarchical-convert-to-date NPM Downloads @adaskothebeast/hierarchical-convert-to-date-fns NPM Downloads @adaskothebeast/hierarchical-convert-to-dayjs NPM Downloads @adaskothebeast/hierarchical-convert-to-js-joda NPM Downloads @adaskothebeast/hierarchical-convert-to-luxon NPM Downloads @adaskothebeast/hierarchical-convert-to-moment NPM Downloads @adaskothebeast/react-redux-toolkit-hierarchical-date-hook


🎯 Why This Library?

Working with dates in JSON is painful. Dates come as strings like "2023-01-15T10:30:00.000Z", forcing you to manually parse them everywhere:

// ❌ Without date-interceptors
const response = await api.get('/users');
const user = response.data;
const createdAt = new Date(user.createdAt);  // Manual parsing
const updatedAt = new Date(user.profile.updatedAt);  // Nested? More parsing!
const postDates = user.posts.map(p => new Date(p.publishedAt));  // Arrays? Loop!
// ✅ With date-interceptors
const response = await api.get('/users');
const user = response.data;
const createdAt = user.createdAt;  // Already a Date object! 🎉
const updatedAt = user.profile.updatedAt;  // Nested? Converted!
const postDates = user.posts.map(p => p.publishedAt);  // Arrays? Handled!

One-time setup. Automatic conversion. Forever.


✨ Features

Core Features

  • 🔄 Automatic Conversion — ISO 8601 date strings → Date objects, no manual parsing
  • 🌳 Deep Traversal — Handles arbitrarily nested objects and arrays
  • ⏱️ Duration Support — ISO 8601 durations (P1Y2M3DT4H5M6S) converted too
  • 🌍 Timezone Aware — Preserves timezone information correctly
  • 📦 Multiple Date Libraries — Supports Date, date-fns, Day.js, Moment.js, Luxon, js-joda
  • 🎨 Framework Ready — Angular v22 interceptors, React v19 helpers, Axios plugins

Security & Performance (NEW!)

  • 🔒 Prototype Pollution Protection — Safe against malicious __proto__ payloads
  • 🔁 Circular Reference Handling — No infinite loops or stack overflows
  • 10-100x Faster — Smart fast-path validation (99% reduction in regex)
  • 🛡️ Crash-Proof — Graceful error handling for invalid dates
  • 💎 Immutable — Deep cloning prevents unintended mutations
  • 📏 Depth Limited — Protects against deeply nested attacks (100 levels max)
  • Type-Safe — Comprehensive TypeScript definitions

📊 Quick Stats

Metric Value
Security Review ✅ OWASP Top 10 compliant
Performance 10-100x faster than naive regex
Test Coverage 130+ tests, all passing
Type Safety Full TypeScript support
Bundle Size Minimal (tree-shakeable)
Dependencies Zero (except date library of choice)
Backward Compatible 100% (v8.0.0+)

🔢 Current Framework/Runtime Versions

Package Version
@angular/core 22.0.7
@angular/common 22.0.7
react 19.2.7
react-dom 19.2.7
rxjs ~7.8.2
@reduxjs/toolkit ^2.12.0
axios 1.18.1

🚀 Quick Start

1. Install

Choose your date library:

# Native JavaScript Date
npm install @adaskothebeast/hierarchical-convert-to-date

# date-fns
npm install @adaskothebeast/hierarchical-convert-to-date-fns

# Day.js
npm install @adaskothebeast/hierarchical-convert-to-dayjs

# Moment.js
npm install @adaskothebeast/hierarchical-convert-to-moment

# Luxon
npm install @adaskothebeast/hierarchical-convert-to-luxon

# js-joda
npm install @adaskothebeast/hierarchical-convert-to-js-joda

2. Use

import { hierarchicalConvertToDate } from '@adaskothebeast/hierarchical-convert-to-date';

const apiResponse = {
  user: {
    name: 'John Doe',
    createdAt: '2023-01-15T10:30:00.000Z',
    profile: {
      birthday: '1990-05-20T00:00:00.000Z'
    },
    posts: [
      { title: 'Hello', publishedAt: '2023-03-01T08:00:00.000Z' },
      { title: 'World', publishedAt: '2023-03-15T14:30:00.000Z' }
    ]
  }
};

hierarchicalConvertToDate(apiResponse);

// All date strings are now Date objects!
console.log(apiResponse.user.createdAt instanceof Date);  // ✅ true
console.log(apiResponse.user.profile.birthday instanceof Date);  // ✅ true
console.log(apiResponse.user.posts[0].publishedAt instanceof Date);  // ✅ true

Typewriter runtime schemas

Typewriter-generated runtime schemas hydrate ordinary JSON into Decimal, UUID, and Temporal values without guessing from string contents. The same schema can serialize rich values back to their wire representation.

npm install @adaskothebeast/typewriter-schema \
  @adaskothebeast/typewriter-runtime
import { serializeJson, transformJson } from '@adaskothebeast/typewriter-runtime';

const invoice = transformJson(responseJson, InvoiceSchema, apiTypeRegistry);
const requestJson = serializeJson(invoice, InvoiceSchema, apiTypeRegistry);

Choose an adapter for the HTTP transport:

Transport Package
Angular HttpClient and httpResource @adaskothebeast/typewriter-http-angular
Axios @adaskothebeast/typewriter-http-axios
Native Fetch @adaskothebeast/typewriter-http-fetch

React and Vue applications use the Fetch or Axios adapter selected by the application. They do not require framework-specific Typewriter packages.

Angular configuration does not select a backend, so the same interceptor works with the default Fetch backend and withXhr():

import { provideHttpClient } from '@angular/common/http';
import {
  withTypewriterHttpInterceptor,
  withTypewriterResponseSchema,
} from '@adaskothebeast/typewriter-http-angular';

export const appConfig = {
  providers: [
    provideHttpClient(withTypewriterHttpInterceptor()),
  ],
};

this.http.get('/api/invoices/1', {
  context: withTypewriterResponseSchema(InvoiceSchema, {
    registry: apiTypeRegistry,
  }),
});

See each package README for request serialization, strict mode, and custom transformer configuration.


Framework and API integration

HTTP transports

Native Fetch

Native Fetch has no interceptor pipeline. Use the exported wrapper so conversion happens at the API boundary:

import { fetchJson } from '@adaskothebeast/hierarchical-convert-to-date';

const user = await fetchJson<User>('/api/users/1');

fetchJson forwards RequestInit, rejects unsuccessful responses with FetchJsonError, handles 204 No Content, and converts nested dates before returning data.

Angular HttpClient

The functional interceptor is the recommended standalone configuration:

import { provideHttpClient } from '@angular/common/http';
import {
  HIERARCHICAL_DATE_ADJUST_FUNCTION,
  withHierarchicalDateHttpInterceptor,
} from '@adaskothebeast/angular-date-http-interceptor';
import { hierarchicalConvertToDate } from '@adaskothebeast/hierarchical-convert-to-date';

export const appConfig = {
  providers: [
    {
      provide: HIERARCHICAL_DATE_ADJUST_FUNCTION,
      useValue: hierarchicalConvertToDate,
    },
    provideHttpClient(withHierarchicalDateHttpInterceptor()),
  ],
};

The interceptor runs at the HttpClient level, so it applies to normal calls and httpResource:

const user$ = http.get<User>('/api/users/1');
const userResource = httpResource<User>(() => '/api/users/1');
Angular configuration Backend Date interceptor Upload progress
provideHttpClient() Fetch Yes No
provideHttpClient(withXhr()) XHR Yes Yes
httpResource() Configured HttpClient Yes Not intended for uploads

Use an independently configured route or lazy-module client for uploads:

{
  path: 'upload',
  providers: [
    provideHttpClient(
      withXhr(),
      withHierarchicalDateHttpInterceptor(),
    ),
  ],
  loadComponent: () => import('./upload.component')
    .then(module => module.UploadComponent),
}

The class-based AngularDateHttpInterceptorModule remains available for existing NgModule applications.

Axios

import { AxiosInstanceManager } from '@adaskothebeast/axios-interceptor';
import { hierarchicalConvertToDate } from '@adaskothebeast/hierarchical-convert-to-date';

export const api = AxiosInstanceManager.createInstance(
  hierarchicalConvertToDate,
);

const response = await api.get<User>('/api/users/1');

Server-state libraries

TanStack Query v5

Convert inside queryFn, before data enters the query cache:

import { useQuery } from '@tanstack/react-query';
import { fetchJson } from '@adaskothebeast/hierarchical-convert-to-date';

const query = useQuery({
  queryKey: ['users'],
  queryFn: () => fetchJson<User[]>('/api/users'),
});

RTK Query

Use transformResponse for one endpoint:

import {
  createHierarchicalDateTransformResponse,
} from '@adaskothebeast/react-redux-toolkit-hierarchical-date-hook';
import { hierarchicalConvertToDate } from '@adaskothebeast/hierarchical-convert-to-date';

getUser: build.query<User, number>({
  query: id => `/users/${id}`,
  transformResponse: createHierarchicalDateTransformResponse<User>(
    hierarchicalConvertToDate,
  ),
}),

Or wrap the base query once so queries and mutations are converted before caching:

import { fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import {
  withHierarchicalDateConversion,
} from '@adaskothebeast/react-redux-toolkit-hierarchical-date-hook';

const rawBaseQuery = fetchBaseQuery({ baseUrl: '/api' });
const baseQuery = withHierarchicalDateConversion(
  rawBaseQuery,
  hierarchicalConvertToDate,
);

The existing hook-level adapter remains available for compatibility, but it converts after data has already entered the RTK Query cache.

SWR

import useSWR from 'swr';
import { fetchJson } from '@adaskothebeast/hierarchical-convert-to-date';

const { data } = useSWR<User[]>('/api/users', fetchJson);

GraphQL

Apollo Link can transform GraphQL response data before it reaches the cache:

import {
  ApolloClient,
  ApolloLink,
  HttpLink,
  InMemoryCache,
} from '@apollo/client';
import { hierarchicalConvertToDate } from '@adaskothebeast/hierarchical-convert-to-date';

const dateLink = new ApolloLink((operation, forward) =>
  forward(operation).map(result => {
    if (result.data) {
      hierarchicalConvertToDate(result.data);
    }
    return result;
  }),
);

const client = new ApolloClient({
  link: ApolloLink.from([
    dateLink,
    new HttpLink({ uri: '/graphql' }),
  ]),
  cache: new InMemoryCache(),
});

Prefer scalar-aware code generation when the GraphQL schema declares exact Date, DateTime, or other temporal scalar semantics.

Generated clients and custom transports

Place conversion in the generated-client boundary, such as a Typewriter service, Orval mutator, openapi-fetch middleware, or NSwag client:

import { fetchJson } from '@adaskothebeast/hierarchical-convert-to-date';

export function generatedClientMutator<T>(
  config: { url: string; init?: RequestInit },
): Promise<T> {
  return fetchJson<T>(config.url, config.init);
}

For property-exact Decimal, UUID, and Temporal hydration, generate schemas and use the Typewriter runtime packages.

State-management recipes

Redux Saga

import { call, put } from 'redux-saga/effects';
import { fetchJson } from '@adaskothebeast/hierarchical-convert-to-date';

function* fetchData(action) {
  const data = yield call(fetchJson, action.payload.url);
  yield put({ type: 'FETCH_SUCCESS', payload: data });
}

Redux Thunk

import { fetchJson } from '@adaskothebeast/hierarchical-convert-to-date';

function fetchApiData(url: string) {
  return async (dispatch: Function) => {
    const data = await fetchJson(url);
    dispatch({ type: 'FETCH_SUCCESS', payload: data });
  };
}

Streaming messages

Convert WebSocket or Server-Sent Event payloads immediately after parsing:

socket.addEventListener('message', event => {
  const message = JSON.parse(event.data);
  hierarchicalConvertToDate(message);
  handleMessage(message);
});

Fetch wrappers such as ky and ofetch, and RxJS ajax, can use the same boundary pattern. Dedicated framework packages are unnecessary unless they provide a cache or transport lifecycle that requires a specific hook.


🔒 Security (NEW in v8.0.0+)

✅ Production-Hardened

This library has undergone comprehensive security review and hardening:

Security Feature Status Impact
Prototype Pollution Protection Blocks __proto__, constructor, prototype
Circular Reference Detection No infinite loops or stack overflows
Depth Limiting Max 100 levels (DoS protection)
Error Handling Graceful degradation on invalid dates
Content-Type Validation Strict application/json only
Immutable Operations Deep cloning prevents mutations

🔴 Critical Fix: Prototype Pollution

Problem:

// Malicious payload
const evil = {
  "__proto__": { "isAdmin": true },
  "date": "2023-01-01T00:00:00.000Z"
};
// Could pollute Object.prototype! 😱

Solution:

// Now safely ignored
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
// ✅ Your app is safe

🟡 High Priority: Performance

Before:

1000 string fields in JSON → 1000 regex tests
CPU intensive, slow on large payloads

After:

1000 string fields → ~10 regex tests (990 fast rejections)
10-100x faster, minimal CPU usage

How?

// Fast character checks BEFORE expensive regex
if (v[4] === '-' && v[7] === '-' && v[10] === 'T') {
  // Only then check regex
}

🛡️ Crash-Proof Error Handling

Before:

// Single invalid date crashed entire conversion
{ "date": "2023-99-99" }  // ❌ Crash!

After:

// Invalid dates remain strings, valid dates converted
{ "date": "2023-99-99" }  // ✅ Left as string
// + Console warning for debugging

⚡ Performance

Benchmarks

Payload Size Strings Dates Before After Improvement
Small 10 2 0.5ms 0.1ms 5x
Medium 100 10 5ms 0.5ms 10x
Large 1000 50 150ms 2ms 75x
Huge 10000 100 3000ms 30ms 100x

Why So Fast?

  1. Fast-path validation — Rejects 99% of non-dates without regex
  2. WeakSet tracking — Efficient circular reference detection
  3. Early bailout — Depth limiting prevents unnecessary work
  4. Zero allocations — In-place mutations (optional deep clone)

📚 Supported Date Libraries

Library Date Type Duration Type Package
Native Date Date N/A hierarchical-convert-to-date
date-fns Date Duration hierarchical-convert-to-date-fns
Day.js Dayjs Duration hierarchical-convert-to-dayjs
Moment.js Moment Duration hierarchical-convert-to-moment
Luxon DateTime Duration hierarchical-convert-to-luxon
js-joda ZonedDateTime N/A hierarchical-convert-to-js-joda

🎨 Framework Integrations

Framework or transport Package Type Features
Native Fetch hierarchical-convert-to-date Fetch wrapper Converts JSON before returning it
Angular angular-date-http-interceptor Functional/class interceptor Fetch, XHR, and httpResource support
Angular angular-typed-http-client Typed client Class-based DTOs and bidirectional transform
Axios axios-interceptor Instance manager Axios response conversion
RTK Query react-redux-toolkit-hierarchical-date-hook Base query/endpoint helpers Converts before caching
Typewriter typewriter-http-angular, typewriter-http-axios, typewriter-http-fetch Schema-aware adapters Decimal, UUID, Temporal, and custom types

🧪 Testing

Security Tests

describe('Security', () => {
  it('blocks prototype pollution', () => {
    const evil = { __proto__: { polluted: true } };
    hierarchicalConvertToDate(evil);
    expect(Object.prototype).not.toHaveProperty('polluted'); 
  });

  it('handles circular references', () => {
    const circular: any = { date: '2023-01-01T00:00:00.000Z' };
    circular.self = circular;
    expect(() => hierarchicalConvertToDate(circular)).not.toThrow(); 
  });

  it('limits depth to 100', () => {
    let deep: any = { date: '2023-01-01T00:00:00.000Z' };
    for (let i = 0; i < 1000; i++) {
      deep = { nested: deep };
    }
    expect(() => hierarchicalConvertToDate(deep)).not.toThrow(); 
  });
});

Coverage

  • 130+ tests across all libraries
  • 100% coverage of security fixes
  • Edge cases tested (invalid dates, null, circular refs)
  • Performance benchmarks included

📖 API Reference

hierarchicalConvertToDate(obj, depth?, visited?)

Recursively converts ISO 8601 date strings to Date objects.

Parameters:

  • obj: unknown — The object/array to process (mutated in place)
  • depth?: number — Current recursion depth (default: 0, max: 100)
  • visited?: WeakSet — Visited objects tracker (default: new WeakSet())

Returns: void (mutates input object)

Examples:

// Simple object
const data = { date: '2023-01-01T00:00:00.000Z' };
hierarchicalConvertToDate(data);
console.log(data.date instanceof Date);  // true

// Nested
const nested = {
  user: {
    profile: {
      birthday: '1990-01-01T00:00:00.000Z'
    }
  }
};
hierarchicalConvertToDate(nested);
// All levels converted!

// Arrays
const arr = [
  { date: '2023-01-01T00:00:00.000Z' },
  { date: '2023-02-01T00:00:00.000Z' }
];
hierarchicalConvertToDate(arr);
// Both converted!

// Mixed
const mixed = {
  name: 'John',
  age: 30,
  active: true,
  metadata: null,
  dates: ['2023-01-01T00:00:00.000Z', '2023-02-01T00:00:00.000Z']
};
hierarchicalConvertToDate(mixed);
// Only date strings converted, rest untouched

fetchJson<T>(input, init?, options?)

Fetches JSON and runs hierarchicalConvertToDate before returning it.

  • input: RequestInfo | URL
  • init?: RequestInit
  • options.fetch?: typeof fetch for custom runtimes and tests
  • Throws FetchJsonError for unsuccessful HTTP responses

Angular functional integration

  • hierarchicalDateHttpInterceptorFn
  • withHierarchicalDateHttpInterceptor()
  • HIERARCHICAL_DATE_ADJUST_FUNCTION

The older HierarchicalDateHttpInterceptor and AngularDateHttpInterceptorModule remain available.

RTK Query integration

  • createHierarchicalDateTransformResponse(convert)
  • withHierarchicalDateConversion(baseQuery, convert)
  • useAdjustUseQueryHookResultWithHierarchicalDateConverter(...)

Prefer the first two APIs because they convert data before cache insertion.


🔧 TypeScript Support

Comprehensive Types

/**
 * Value types that can appear in converted data
 */
type DateValue = Date | string | number | boolean | null;

/**
 * Object with potentially date-convertible fields
 */
type DateObject = { [key: string]: DateValue | DateObject | DateArray };

/**
 * Array of potentially date-convertible values
 */
type DateArray = Array<DateValue | DateObject | DateArray>;

/**
 * Root type for conversion
 */
type RecordWithDate = DateObject;

Full IDE Support

  • ✅ Autocompletion for all methods
  • ✅ Type inference for nested structures
  • ✅ JSDoc documentation
  • ✅ Error hints and warnings

🚨 Breaking Changes & Migration

v7.0.0 → v8.0.0+ (Axios Only)

What Changed:
Axios AxiosInstanceManager no longer caches instances (singleton pattern removed).

Before:

const instance1 = AxiosInstanceManager.createInstance(convertFunc);
const instance2 = AxiosInstanceManager.createInstance(convertFunc);
// instance1 === instance2 ✅ (cached)

After:

const instance1 = AxiosInstanceManager.createInstance(convertFunc);
const instance2 = AxiosInstanceManager.createInstance(convertFunc);
// instance1 !== instance2 ⚠️ (new instances)

Migration:

// Create once, export, reuse
export const api = AxiosInstanceManager.createInstance(hierarchicalConvertToDate);

// Import and use everywhere
import { api } from './api';
const response = await api.get('/users');

Everything Else

100% backward compatible! All other changes are non-breaking.


🐛 Troubleshooting

Invalid dates remain strings

Problem:

const data = { date: '2023-99-99T99:99:99.000Z' };
hierarchicalConvertToDate(data);
console.log(data.date);  // Still a string? 🤔

Solution:
This is expected behavior. Invalid date strings are left unchanged (graceful degradation). Check console for warnings:

⚠️ Failed to parse date string: 2023-99-99T99:99:99.000Z

Performance issues

Problem:
Conversion still slow on large payloads?

Solutions:

  1. ✅ Upgrade to v8.0.0+ (10-100x faster)
  2. ✅ Profile your data — are there really many date strings?
  3. ✅ Consider server-side conversion for massive payloads (>100MB)

TypeScript errors

Problem:

Type 'unknown' is not assignable to type 'Date'

Solution:
Use type assertions or type guards:

const data = apiResponse as { date: Date };
// or
if (data.date instanceof Date) {
  // TypeScript knows it's a Date here
}

Circular references warning

Problem:

⚠️ Circular reference detected in object

Solution:
This is expected if your data has circular refs. Conversion still succeeds, but circular paths are skipped.


🎓 Advanced Usage

Custom Depth Limit

// Default is 100, but you can customize
function convertShallow(obj: unknown) {
  hierarchicalConvertToDate(obj, 0, new WeakSet());
  // Will stop at depth 100 (depth param is current depth, not max)
}

Performance Monitoring

function convertWithTiming(obj: unknown) {
  const start = performance.now();
  hierarchicalConvertToDate(obj);
  const end = performance.now();
  console.log(`Conversion took ${end - start}ms`);
}

Conditional Conversion

function convertIfNeeded(obj: unknown, shouldConvert: boolean) {
  if (shouldConvert && obj != null && typeof obj === 'object') {
    hierarchicalConvertToDate(obj);
  }
}

🎁 BONUS: Angular Typed HTTP Client

For advanced Angular developers: If you need more than simple date conversion, check out our Type-Safe HTTP
Client with class-transformer integration!

Why Use It?

  • 🎯 Full Type Safety — Compile-time + runtime type checking with class constructors
  • 🔄 Bidirectional Transform — Serialize requests AND deserialize responses automatically
  • 🏷️ Decorator-Based — Use @Transform, @Type, @Expose, @Exclude for custom logic
  • 📦 DTO Pattern — Clean separation of API models from domain models
  • Validation Ready — Seamless integration with class-validator
  • 💎 Computed Properties — Add getters and methods to your response objects
  • 🔥 .NET Integration — Perfect for Newtonsoft.Json/System.Text.Json polymorphic types
  • 📝 Typewriter Support — Auto-generate TypeScript classes from C# models

Quick Example

import { Transform, Type, Expose, Exclude } from 'class-transformer';
import { IsEmail, IsNotEmpty } from 'class-validator';

class AddressDto {
  @Expose()
  street!: string;
  
  @Expose()
  city!: string;
}

class UserDto {
  @Expose()
  @IsNotEmpty()
  id!: number;
  
  @Expose()
  @IsEmail()
  email!: string;
  
  @Exclude()  // Won't be sent or received
  password?: string;
  
  @Transform(({ value }) => new Date(value), { toClassOnly: true })
  @Transform(({ value }) => value?.toISOString(), { toPlainOnly: true })
  createdAt!: Date;
  
  @Type(() => AddressDto)
  address?: AddressDto;
  
  @Type(() => PostDto)
  posts?: PostDto[];
  
  // Computed property
  get isRecent(): boolean {
    const dayAgo = new Date();
    dayAgo.setDate(dayAgo.getDate() - 1);
    return this.createdAt > dayAgo;
  }
}

// POST with automatic serialization
const newUser = new UserDto();
newUser.email = 'john@example.com';
newUser.createdAt = new Date();

typedHttp.post('/api/users', newUser, UserDto).subscribe(savedUser => {
  console.log(savedUser instanceof UserDto);  // ✅ true
  console.log(savedUser.isRecent);  // ✅ Works!
});

API Methods:

// Get response body only
typedHttp.get<T>(url, Ctor, options?): Observable<T>
typedHttp.post<T, K>(url, body, Ctor, options?): Observable<K>
typedHttp.put<T, K>(url, body, Ctor, options?): Observable<K>
typedHttp.patch<T, K>(url, body, Ctor, options?): Observable<K>
typedHttp.delete<K>(url, Ctor, options?): Observable<K>

// Get full HttpResponse
typedHttp.getResponse<K>(url, Ctor, options?): Observable<HttpResponse<K>>
typedHttp.postResponse<T, K>(url, body, Ctor, options?): Observable<HttpResponse<K>>
// ... etc

Options:

const options: RequestOptions = {
  headers: { 'Authorization': 'Bearer token' },
  params: { page: '1', limit: '10' },
  serialize: true,  // Auto-serialize request body (default: true)
  // or use class-transformer options:
  serialize: {
    excludeExtraneousValues: true,
    enableImplicitConversion: true
  }
};

Why use Typed HTTP Client over simple interceptor?

Feature Interceptor Typed HTTP Client
Date conversion ✅ Automatic ✅ Automatic + custom
Type safety ⚠️ Runtime only ✅ Compile-time + Runtime
Request serialization ❌ No ✅ Yes
Nested objects ✅ Yes ✅ Yes + validation
Custom transforms ❌ No ✅ Full decorator support
Class methods ❌ No ✅ Yes (computed props, etc.)
Validation ❌ No ✅ class-validator integration

Use Typed HTTP Client when:

  • ✅ You want compile-time type safety
  • ✅ You need bidirectional transformation (request + response)
  • ✅ You're using DTOs/class-based architecture
  • ✅ You need validation with class-validator
  • ✅ You want computed properties on response objects

Use simple interceptor when:

  • ✅ You only need date conversion (no other transforms)
  • ✅ You work with plain objects (no classes)
  • ✅ You want minimal setup
  • ✅ You don't need request serialization

🔥 .NET Integration: Polymorphic Types

Perfect for .NET developers! If you're using Newtonsoft.Json or System.Text.Json with polymorphic types, the Typed HTTP Client handles them beautifully with the Typewriter Visual Studio extension.

The Problem: .NET Polymorphic Serialization

.NET APIs often return polymorphic types with discriminators:

C# Model (Newtonsoft.Json):

// Base class
[JsonConverter(typeof(JsonSubtypes), "$type")]
[JsonSubtypes.KnownSubType(typeof(EmailNotification), "Email")]
[JsonSubtypes.KnownSubType(typeof(SmsNotification), "Sms")]
[JsonSubtypes.KnownSubType(typeof(PushNotification), "Push")]
public abstract class Notification
{
    // $type is automatically generated by Newtonsoft.Json
    public DateTime CreatedAt { get; set; }
    public string Message { get; set; }
}

public class EmailNotification : Notification
{
    public string To { get; set; }
    public string Subject { get; set; }
    public string HtmlBody { get; set; }
}

public class SmsNotification : Notification
{
    public string PhoneNumber { get; set; }
    public string ShortCode { get; set; }
}

public class PushNotification : Notification
{
    public string DeviceToken { get; set; }
    public string Title { get; set; }
    public Dictionary<string, string> Data { get; set; }
}

C# Model (System.Text.Json - .NET 7+):

// You can choose any discriminator property name
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]  // or "type", "kind", etc.
[JsonDerivedType(typeof(EmailNotification), "email")]
[JsonDerivedType(typeof(SmsNotification), "sms")]
[JsonDerivedType(typeof(PushNotification), "push")]
public abstract class Notification
{
    public DateTime CreatedAt { get; set; }
    public string Message { get; set; }
}

// Alternative with custom discriminator
[JsonPolymorphic(TypeDiscriminatorPropertyName = "notificationType")]
[JsonDerivedType(typeof(EmailNotification), "email")]
[JsonDerivedType(typeof(SmsNotification), "sms")]
public abstract class NotificationV2 { /* ... */ }

JSON Response (Newtonsoft.Json):

{
  "notifications": [
    {
      "$type": "Email",
      "createdAt": "2023-01-15T10:30:00.000Z",
      "message": "Welcome!",
      "to": "user@example.com",
      "subject": "Welcome to our app",
      "htmlBody": "<h1>Welcome!</h1>"
    },
    {
      "$type": "Sms",
      "createdAt": "2023-01-15T11:00:00.000Z",
      "message": "Your code: 123456",
      "phoneNumber": "+1234567890",
      "shortCode": "12345"
    },
    {
      "$type": "Push",
      "createdAt": "2023-01-15T12:00:00.000Z",
      "message": "New message",
      "deviceToken": "abc123...",
      "title": "You have a new message",
      "data": { "messageId": "456" }
    }
  ]
}

The Solution: Typewriter + class-transformer

Step 1: Generate TypeScript with Typewriter

Install Typewriter extension in Visual Studio, then create a .tst template:

💡 Pro Tip: Complete .tst template recipes for Angular and React (both Newtonsoft.Json and System.Text.Json) are available at NetCoreTypewriterRecipes!

${
    using Typewriter.Extensions.Types;
    
    Template(Settings settings)
    {
        settings.IncludeProject("YourApi.Models");
        settings.OutputExtension = ".ts";
    }
    
    string Imports(Class c) => c.BaseClass != null 
        ? $"import {{ {c.BaseClass.Name} }} from './{c.BaseClass.Name}';"
        : "";
}
$Classes(*Notification)[
import { Transform, Type } from 'class-transformer';
$Imports

export class $Name$TypeParameters {
    $Properties[
    $Attributes[Transform][    @Transform(({ value }) => new Date(value), { toClassOnly: true })]
    $Name: $Type;
    ]
}
]

Generated TypeScript:

// notification.base.ts
import { Transform } from 'class-transformer';

export abstract class Notification {
  // $type is automatically handled by class-transformer discriminator
  
  @Transform(({ value }) => new Date(value), { toClassOnly: true })
  createdAt!: Date;
  
  message!: string;
}

// email-notification.ts
import { Notification } from './notification.base';

export class EmailNotification extends Notification {
  to!: string;
  subject!: string;
  htmlBody!: string;
}

// sms-notification.ts
import { Notification } from './notification.base';

export class SmsNotification extends Notification {
  phoneNumber!: string;
  shortCode!: string;
}

// push-notification.ts
import { Notification } from './notification.base';

export class PushNotification extends Notification {
  deviceToken!: string;
  title!: string;
  data!: Record<string, string>;
}

Step 2: Add Discriminator Configuration

Create a factory that uses the discriminator:

import { Transform, Type } from 'class-transformer';
import { Notification } from './notification.base';
import { EmailNotification } from './email-notification';
import { SmsNotification } from './sms-notification';
import { PushNotification } from './push-notification';

export abstract class NotificationBase extends Notification {
  @Transform(({ value }) => new Date(value), { toClassOnly: true })
  createdAt!: Date;
  
  // Discriminator-based transformation (Newtonsoft.Json uses $type)
  @Type(() => NotificationBase, {
    discriminator: {
      property: '$type',  // Newtonsoft.Json default
      subTypes: [
        { value: EmailNotification, name: 'Email' },
        { value: SmsNotification, name: 'Sms' },
        { value: PushNotification, name: 'Push' },
      ],
    },
  })
  static createFromType(data: any): Notification {
    // class-transformer handles this automatically
    return data;
  }
}

export class NotificationListDto {
  @Type(() => NotificationBase, {
    discriminator: {
      property: '$type',  // Match your C# configuration
      subTypes: [
        { value: EmailNotification, name: 'Email' },
        { value: SmsNotification, name: 'Sms' },
        { value: PushNotification, name: 'Push' },
      ],
    },
  })
  notifications!: Notification[];
}

Step 3: Use in Your Component

import { Component, inject } from '@angular/core';
import { TypedHttpClient } from '@adaskothebeast/angular-typed-http-client';
import { NotificationListDto, EmailNotification, SmsNotification, PushNotification } from './models';

@Component({
  selector: 'app-notifications',
  template: `
    <div *ngFor="let notification of (notifications$ | async)?.notifications">
      <!-- Type guards work! -->
      <div *ngIf="isEmail(notification)" class="email">
        📧 Email to {{ notification.to }}: {{ notification.subject }}
        <div [innerHTML]="notification.htmlBody"></div>
      </div>
      
      <div *ngIf="isSms(notification)" class="sms">
        💬 SMS to {{ notification.phoneNumber }}: {{ notification.message }}
      </div>
      
      <div *ngIf="isPush(notification)" class="push">
        📱 Push to device: {{ notification.title }}
        <pre>{{ notification.data | json }}</pre>
      </div>
      
      <!-- Date is already converted! -->
      <small>{{ notification.createdAt | date:'short' }}</small>
    </div>
  `
})
export class NotificationsComponent {
  private typedHttp = inject(TypedHttpClient);
  
  notifications$ = this.typedHttp.get('/api/notifications', NotificationListDto);
  
  // Type guards for template
  isEmail(n: Notification): n is EmailNotification {
    return n instanceof EmailNotification;
  }
  
  isSms(n: Notification): n is SmsNotification {
    return n instanceof SmsNotification;
  }
  
  isPush(n: Notification): n is PushNotification {
    return n instanceof PushNotification;
  }
  
  // Or use type property
  getNotificationType(notification: Notification): string {
    if (notification instanceof EmailNotification) return 'email';
    if (notification instanceof SmsNotification) return 'sms';
    if (notification instanceof PushNotification) return 'push';
    return 'unknown';
  }
}

Step 4: Polymorphic POST/PUT Requests

Sending polymorphic types back to .NET:

// Create different notification types
const emailNotif = new EmailNotification();
// $type is automatically added during serialization
emailNotif.to = 'user@example.com';
emailNotif.subject = 'Test';
emailNotif.message = 'Hello!';
emailNotif.htmlBody = '<p>Hello World!</p>';
emailNotif.createdAt = new Date();

const smsNotif = new SmsNotification();
// $type is automatically added during serialization
smsNotif.phoneNumber = '+1234567890';
smsNotif.message = 'Your code: 123';
smsNotif.createdAt = new Date();

// Send to API - automatically serialized with discriminator!
this.typedHttp.post('/api/notifications', emailNotif, EmailNotification)
  .subscribe(result => {
    console.log('Saved:', result);
    console.log(result instanceof EmailNotification);  // ✅ true
    console.log(result.createdAt instanceof Date);  // ✅ true
  });

Benefits for .NET Developers

Feature Without Typed Client With Typed Client
Polymorphic Types ❌ Manual type checking ✅ Automatic with discriminator
Type Safety ⚠️ as casts everywhere ✅ True instanceof checks
Date Conversion ❌ Manual parsing ✅ Automatic with @Transform
Typewriter Integration ⚠️ Manual class creation ✅ Auto-generated from C#
Validation ❌ Runtime only ✅ Compile-time + Runtime
Serialization ❌ Manual JSON.stringify ✅ Automatic with decorators
Nested Types ⚠️ Complex manual handling ✅ @Type decorator handles it
Discriminator ❌ Manual switch/case ✅ class-transformer handles it

System.Text.Json Configuration

For System.Text.Json, the discriminator property is configurable in C#:

// Match your C# TypeDiscriminatorPropertyName setting
export class NotificationListDto {
  @Type(() => NotificationBase, {
    discriminator: {
      property: '$type',  // or 'type', 'kind', 'notificationType', etc.
      subTypes: [
        { value: EmailNotification, name: 'email' },      // lowercase in .NET 7+
        { value: SmsNotification, name: 'sms' },
        { value: PushNotification, name: 'push' },
      ],
    },
  })
  notifications!: Notification[];
}

// If you used custom discriminator in C#:
// [JsonPolymorphic(TypeDiscriminatorPropertyName = "notificationType")]
export class CustomNotificationListDto {
  @Type(() => NotificationBase, {
    discriminator: {
      property: 'notificationType',  // Must match C# configuration!
      subTypes: [
        { value: EmailNotification, name: 'email' },
        { value: SmsNotification, name: 'sms' },
      ],
    },
  })
  notifications!: Notification[];
}

Advanced: Deeply Nested Polymorphism

// C# - Nested polymorphic types
public class NotificationGroup
{
    public string Name { get; set; }
    public List<Notification> Notifications { get; set; }
    public NotificationSettings Settings { get; set; }
}

[JsonPolymorphic]
[JsonDerivedType(typeof(EmailSettings), "email")]
[JsonDerivedType(typeof(SmsSettings), "sms")]
public abstract class NotificationSettings
{
    public bool Enabled { get; set; }
}
// TypeScript - Nested discriminators work too!
export class NotificationGroupDto {
  name!: string;
  
  @Type(() => NotificationBase, {
    discriminator: {
      property: '$type',  // Newtonsoft.Json
      subTypes: [
        { value: EmailNotification, name: 'Email' },
        { value: SmsNotification, name: 'Sms' },
      ],
    },
  })
  notifications!: Notification[];
  
  @Type(() => NotificationSettingsBase, {
    discriminator: {
      property: '$type',  // Both can use same or different discriminators
      subTypes: [
        { value: EmailSettings, name: 'email' },
        { value: SmsSettings, name: 'sms' },
      ],
    },
  })
  settings!: NotificationSettings;
}

Result: Automatic deserialization of nested polymorphic hierarchies! 🎉

Resources


🤝 Contributing

We welcome contributions! To get started:

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/amazing-feature
  3. Make your changes
  4. Add tests for new functionality
  5. Ensure all tests pass: yarn test:all
  6. Commit: git commit -m 'Add amazing feature'
  7. Push: git push origin feature/amazing-feature
  8. Open a Pull Request

Development Setup

# Clone
git clone https://github.com/AdaskoTheBeAsT/date-interceptors.git
cd date-interceptors

# Install
yarn install

# Test
yarn test:all

# Build
yarn build:all

# Lint
yarn lint:all

🔐 Security

Reporting Vulnerabilities

If you discover a security vulnerability, please email:
📧 adaskothebeast@gmail.com

Please include:

  • Description of the vulnerability
  • Steps to reproduce
  • Potential impact
  • Suggested fix (if any)

We take security seriously and will respond promptly.

Security Audits

  • ✅ OWASP Top 10 reviewed
  • ✅ CWE-1321 (Prototype Pollution) mitigated
  • ✅ DoS protection (depth limiting)
  • ✅ Input validation hardened
  • ✅ Error handling comprehensive

📄 License

MIT © AdaskoTheBeAsT


🌟 Show Your Support

If this library saves you time, give it a ⭐ on GitHub!


📞 Support


🎉 Acknowledgments

Thanks to all contributors and the community for making this library better!

Special thanks to:

  • OWASP for security guidelines
  • Date library maintainers for excellent date/time tooling
  • Framework teams for making integration smooth

Made with ❤️ by developers, for developers

⬆ Back to Top