TypeScript Cacheable Decorator
TypeScript Cacheable is an in-memory memoization decorator library designed to cache the results of expensive TypeScript methods or property accessors. Currently at version 3.0.3, it provides a stable API for enhancing application performance by storing computed values for subsequent retrieval. The library distinguishes itself by offering flexible caching scopes: 'GLOBAL' for application-wide caching, and 'LOCAL_STORAGE' which leverages Node.js's `AsyncLocalStorage` to provide request-scoped or call-chain-scoped caching, crucial for web applications. Cache keys can be automatically inferred from method parameters if they are JSON-serializable, or explicitly defined by implementing the `CacheableKey` interface for complex objects. While the library itself does not have a stated release cadence, its `3.x` version indicates active development and maintenance.
Common errors
-
Error: Decorators are not enabled. You must enable 'experimentalDecorators' in your tsconfig.json.
cause The TypeScript compiler is not configured to support experimental decorators, which are required for `@Cacheable` to function.fixAdd or update the `compilerOptions` in your `tsconfig.json` file: `"experimentalDecorators": true, "emitDecoratorMetadata": true`. -
TypeError: Cannot convert circular structure to JSON
cause A method decorated with `@Cacheable` received an argument that contains circular references, and the library attempted to JSON-serialize it to generate a cache key.fixFor the problematic parameter, implement the `CacheableKey` interface on its class, providing a `cacheKey()` method that generates a unique, non-circular string. Alternatively, pass a custom `keyComposer` function to the `@Cacheable` decorator options. -
Always re-computing value when using LOCAL_STORAGE scope / Cache is not working as expected with AsyncLocalStorage.
cause The `AsyncLocalStorage` instance is not correctly bound in the call chain, or the `getStore` function provided to `@Cacheable` does not return the same `AsyncLocalStorage` instance that was used to create the execution context.fixVerify that `AsyncLocalStorage.run()` is used to wrap the code path where caching should occur (e.g., in Express middleware) and that the `getStore` function consistently retrieves the correct `AsyncLocalStorage` instance. Refer to the `AsyncLocalStorage` example in the documentation. -
Cannot apply a decorator to a non-method or non-accessor.
cause The `@Cacheable` decorator was applied to a class property directly, a class itself, or another non-callable member, instead of a method or property accessor (getter/setter).fixEnsure `@Cacheable` is only used on class methods or property getters/setters, as decorators in TypeScript modify the behavior of callable members.
Warnings
- gotcha When using `GLOBAL` scope with methods that accept a wide range of unique, non-primitive parameters, the in-memory cache can grow indefinitely, leading to high memory consumption and potential memory leaks if not managed.
- breaking Decorators, while widely used, are still an 'experimental' TypeScript feature. For `typescript-cacheable` to function correctly, `experimentalDecorators` and `emitDecoratorMetadata` must be enabled in your `tsconfig.json` compiler options. Future TypeScript versions might introduce changes to the decorator API.
- gotcha The `LOCAL_STORAGE` caching scope relies on Node.js's `AsyncLocalStorage`. It is critical to correctly bind `AsyncLocalStorage` in your application's call chain (e.g., using middleware in an Express app) and ensure the `getStore` function provided to `@Cacheable` returns the *exact same* `AsyncLocalStorage` instance used for binding. Incorrect setup will result in the cache not being isolated per request or not being used at all.
- gotcha Automatic cache key inference for method parameters relies on JSON serialization. Parameters with circular references, `undefined` values within objects (top-level `undefined` arguments are supported), or non-serializable types will lead to errors or unexpected cache behavior.
- breaking Major version updates (e.g., from v2.x to v3.x) for decorator-based libraries often involve breaking changes due to evolutions in TypeScript's decorator specification or internal implementation details. Always review the changelog when upgrading between major versions.
Install
-
npm install typescript-cacheable -
yarn add typescript-cacheable -
pnpm add typescript-cacheable
Imports
- Cacheable
const Cacheable = require('typescript-cacheable');import { Cacheable } from 'typescript-cacheable'; - CacheableKey
import { CacheableKey } from 'typescript-cacheable'; - CacheableOptions (implicit)
import { Cacheable } from 'typescript-cacheable'; // Example usage with options @Cacheable({ scope: 'GLOBAL', ttl: 60000 })
Quickstart
import { Cacheable } from 'typescript-cacheable';
import { AsyncLocalStorage } from 'async_hooks';
interface Dwarf { name: string; lastName: string; }
// Simulate a context for AsyncLocalStorage (e.g., an HTTP request context)
class Context { constructor(public requestId: string) {} }
const als = new AsyncLocalStorage<Context>();
// Helper to get the store for LOCAL_STORAGE scope
export const getStore = (): unknown => als.getStore();
class DwarfService {
private callCount: number = 0;
// Caching globally without parameters
@Cacheable()
public async findHappiest(): Promise<Dwarf> {
this.callCount++;
return new Promise((resolve) => {
setTimeout(() => {
resolve({ name: 'Huck', lastName: 'Finn' });
}, 100); // Simulate expensive operation
});
}
// Caching with parameters, inferring key from JSON-serializable args
@Cacheable()
public async countByLastName(name: string): Promise<number> {
this.callCount++;
return new Promise((resolve) => {
setTimeout(() => {
resolve(name.length * 5);
}, 50); // Simulate expensive operation
});
}
// Caching with AsyncLocalStorage (request scope)
@Cacheable({ scope: 'LOCAL_STORAGE', getStore: getStore })
public async getRequestScopedValue(): Promise<string> {
const store = als.getStore();
const requestId = store ? store.requestId : 'no-request-id';
this.callCount++;
return new Promise((resolve) => {
setTimeout(() => {
resolve(`Value for req ${requestId}, call ${this.callCount}`);
}, 20); // Simulate expensive operation
});
}
public getCallCount(): number { return this.callCount; }
}
// Example Usage
async function runExamples() {
const service = new DwarfService();
console.log('--- Global Cache Example ---');
const dwarf1 = await service.findHappiest();
console.log('First call (global):', dwarf1, 'Call Count:', service.getCallCount());
const dwarf2 = await service.findHappiest();
console.log('Second call (global, cached):', dwarf2, 'Call Count:', service.getCallCount());
console.log('\n--- Parameterized Cache Example ---');
service['callCount'] = 0; // Reset for demonstration
const count1 = await service.countByLastName('Snow');
console.log('First call (params):', count1, 'Call Count:', service.getCallCount());
const count2 = await service.countByLastName('Snow');
console.log('Second call (params, cached):', count2, 'Call Count:', service.getCallCount());
const count3 = await service.countByLastName('White');
console.log('Third call (params, new arg):', count3, 'Call Count:', service.getCallCount());
console.log('\n--- Local Storage Cache Example (Simulated Request) ---');
service['callCount'] = 0; // Reset for demonstration
await als.run(new Context('req-123'), async () => {
const valA1 = await service.getRequestScopedValue();
console.log('Req 123 - Call 1:', valA1, 'Call Count:', service.getCallCount());
const valA2 = await service.getRequestScopedValue();
console.log('Req 123 - Call 2 (cached):', valA2, 'Call Count:', service.getCallCount());
});
await als.run(new Context('req-456'), async () => {
const valB1 = await service.getRequestScopedValue();
console.log('Req 456 - Call 1:', valB1, 'Call Count:', service.getCallCount());
const valB2 = await service.getRequestScopedValue();
console.log('Req 456 - Call 2 (cached):', valB2, 'Call Count:', service.getCallCount());
});
}
runExamples();