{"id":12226,"library":"typescript-cacheable","title":"TypeScript Cacheable Decorator","description":"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.","status":"active","version":"3.0.3","language":"javascript","source_language":"en","source_url":null,"tags":["javascript","cache","caching","memoize","memoization","typescript","decorator","memoise","memoisation"],"install":[{"cmd":"npm install typescript-cacheable","lang":"bash","label":"npm"},{"cmd":"yarn add typescript-cacheable","lang":"bash","label":"yarn"},{"cmd":"pnpm add typescript-cacheable","lang":"bash","label":"pnpm"}],"dependencies":[],"imports":[{"note":"The Cacheable decorator is a named export. ESM import syntax is standard. CommonJS `require` is generally not suitable for decorators or modern TypeScript modules.","wrong":"const Cacheable = require('typescript-cacheable');","symbol":"Cacheable","correct":"import { Cacheable } from 'typescript-cacheable';"},{"note":"CacheableKey is a TypeScript interface, used for type-checking and defining custom cache key logic. It is a named export and disappears at runtime.","symbol":"CacheableKey","correct":"import { CacheableKey } from 'typescript-cacheable';"},{"note":"Configuration options for the decorator are passed as an object to `Cacheable()`. While `CacheableOptions` is not directly imported as a type, its properties are part of the `Cacheable` decorator's API.","symbol":"CacheableOptions (implicit)","correct":"import { Cacheable } from 'typescript-cacheable';\n// Example usage with options\n@Cacheable({ scope: 'GLOBAL', ttl: 60000 })"}],"quickstart":{"code":"import { Cacheable } from 'typescript-cacheable';\nimport { AsyncLocalStorage } from 'async_hooks';\n\ninterface Dwarf { name: string; lastName: string; }\n\n// Simulate a context for AsyncLocalStorage (e.g., an HTTP request context)\nclass Context { constructor(public requestId: string) {} }\nconst als = new AsyncLocalStorage<Context>();\n\n// Helper to get the store for LOCAL_STORAGE scope\nexport const getStore = (): unknown => als.getStore();\n\nclass DwarfService {\n    private callCount: number = 0;\n\n    // Caching globally without parameters\n    @Cacheable()\n    public async findHappiest(): Promise<Dwarf> {\n        this.callCount++;\n        return new Promise((resolve) => {\n            setTimeout(() => {\n                resolve({ name: 'Huck', lastName: 'Finn' });\n            }, 100); // Simulate expensive operation\n        });\n    }\n\n    // Caching with parameters, inferring key from JSON-serializable args\n    @Cacheable()\n    public async countByLastName(name: string): Promise<number> {\n        this.callCount++;\n        return new Promise((resolve) => {\n            setTimeout(() => {\n                resolve(name.length * 5);\n            }, 50); // Simulate expensive operation\n        });\n    }\n\n    // Caching with AsyncLocalStorage (request scope)\n    @Cacheable({ scope: 'LOCAL_STORAGE', getStore: getStore })\n    public async getRequestScopedValue(): Promise<string> {\n        const store = als.getStore();\n        const requestId = store ? store.requestId : 'no-request-id';\n        this.callCount++;\n        return new Promise((resolve) => {\n            setTimeout(() => {\n                resolve(`Value for req ${requestId}, call ${this.callCount}`);\n            }, 20); // Simulate expensive operation\n        });\n    }\n\n    public getCallCount(): number { return this.callCount; }\n}\n\n// Example Usage\nasync function runExamples() {\n    const service = new DwarfService();\n\n    console.log('--- Global Cache Example ---');\n    const dwarf1 = await service.findHappiest();\n    console.log('First call (global):', dwarf1, 'Call Count:', service.getCallCount());\n    const dwarf2 = await service.findHappiest();\n    console.log('Second call (global, cached):', dwarf2, 'Call Count:', service.getCallCount());\n\n    console.log('\\n--- Parameterized Cache Example ---');\n    service['callCount'] = 0; // Reset for demonstration\n    const count1 = await service.countByLastName('Snow');\n    console.log('First call (params):', count1, 'Call Count:', service.getCallCount());\n    const count2 = await service.countByLastName('Snow');\n    console.log('Second call (params, cached):', count2, 'Call Count:', service.getCallCount());\n    const count3 = await service.countByLastName('White');\n    console.log('Third call (params, new arg):', count3, 'Call Count:', service.getCallCount());\n\n    console.log('\\n--- Local Storage Cache Example (Simulated Request) ---');\n    service['callCount'] = 0; // Reset for demonstration\n\n    await als.run(new Context('req-123'), async () => {\n        const valA1 = await service.getRequestScopedValue();\n        console.log('Req 123 - Call 1:', valA1, 'Call Count:', service.getCallCount());\n        const valA2 = await service.getRequestScopedValue();\n        console.log('Req 123 - Call 2 (cached):', valA2, 'Call Count:', service.getCallCount());\n    });\n\n    await als.run(new Context('req-456'), async () => {\n        const valB1 = await service.getRequestScopedValue();\n        console.log('Req 456 - Call 1:', valB1, 'Call Count:', service.getCallCount());\n        const valB2 = await service.getRequestScopedValue();\n        console.log('Req 456 - Call 2 (cached):', valB2, 'Call Count:', service.getCallCount());\n    });\n}\n\nrunExamples();","lang":"typescript","description":"This quickstart demonstrates `typescript-cacheable` with global, parameterized, and `AsyncLocalStorage`-scoped caching. It shows how to apply the `@Cacheable` decorator, highlights automatic key inference for JSON-serializable arguments, and illustrates the setup required for request-scoped caching using Node.js's `AsyncLocalStorage` to ensure calls within the same 'request' hit the cache, while separate 'requests' compute new values."},"warnings":[{"fix":"Implement the `CacheableKey` interface for complex parameters to generate a controlled, unique cache key, or ensure parameters result in a finite set of cache entries. Consider using `ttl` (Time-To-Live) options to automatically expire cached entries.","message":"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.","severity":"gotcha","affected_versions":">=1.0.0"},{"fix":"Add the following to your `tsconfig.json` under `compilerOptions`: `\"experimentalDecorators\": true, \"emitDecoratorMetadata\": true`.","message":"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.","severity":"breaking","affected_versions":">=1.0.0"},{"fix":"Review the `AsyncLocalStorage` integration example in the documentation. Ensure `als.run()` wraps the entire request context and that the `AsyncLocalStorage` instance is consistently used across your application for binding and retrieval within the decorator.","message":"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.","severity":"gotcha","affected_versions":">=1.0.0"},{"fix":"For complex or non-JSON-serializable parameters, implement the `CacheableKey` interface on the parameter class and provide a `cacheKey()` method that returns a unique string identifier. Alternatively, use a custom `keyComposer` option in the `@Cacheable` decorator.","message":"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.","severity":"gotcha","affected_versions":">=1.0.0"},{"fix":"Consult the official release notes and changelog for `typescript-cacheable` when migrating between major versions to understand specific API changes, deprecations, or required configuration updates.","message":"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.","severity":"breaking","affected_versions":"~2.x to >=3.0.0"}],"env_vars":null,"last_verified":"2026-04-19T00:00:00.000Z","next_check":"2026-07-18T00:00:00.000Z","problems":[{"fix":"Add or update the `compilerOptions` in your `tsconfig.json` file: `\"experimentalDecorators\": true, \"emitDecoratorMetadata\": true`.","cause":"The TypeScript compiler is not configured to support experimental decorators, which are required for `@Cacheable` to function.","error":"Error: Decorators are not enabled. You must enable 'experimentalDecorators' in your tsconfig.json."},{"fix":"For 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.","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.","error":"TypeError: Cannot convert circular structure to JSON"},{"fix":"Verify 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.","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.","error":"Always re-computing value when using LOCAL_STORAGE scope / Cache is not working as expected with AsyncLocalStorage."},{"fix":"Ensure `@Cacheable` is only used on class methods or property getters/setters, as decorators in TypeScript modify the behavior of callable members.","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).","error":"Cannot apply a decorator to a non-method or non-accessor."}],"ecosystem":"npm"}