Type safety and type inference
Since
v2.0
By default the CLS context is untyped and allows setting and retrieving any string
or symbol
keys from the context. Some safety can be enforced by using CONSTANTS
instead of magic strings, but that might not be enough.
Type-safe ClsService
It is possible to specify a custom interface for the ClsService
and get proper typing and automatic type inference when retrieving or setting values. This works even for nested objects using a dot notation.
To create a typed CLS Store, start by creating an interface that extends ClsStore
.
export interface MyClsStore extends ClsStore {
tenantId: string;
user: {
id: number;
authorized: boolean;
};
}
Using a type parameter
Then you can inject the ClsService
with a type parameter ClsService<MyClsStore>
to make use of the safe typing.
export class MyService {
constructor(private readonly cls: ClsService<MyClsStore>) {}
doTheThing() {
// a boolean type will be enforced here
this.cls.set('user.authorized', true);
// tenantId will be inferred as a string
const tenantId = this.cls.get('tenantId');
// userId will be inferred as a number
const userId = this.cls.get('user.id');
// user will be inferred as { id: number, authorized: boolean }
const user = this.cls.get('user');
// you'll even get intellisense for the keys, because the type
// will be inferred as:
// symbol | 'tenantId˙ | 'user' | 'user.id' | 'user.authorized'
// alternatively, since the `get` method returns the whole store
// when called without arguments, you can use object destructuring
const { tenantId, user } = this.cls.get();
// accessing a nonexistent property will result in a type error
const notExist = this.cls.get('user.name');
}
}
Using Typescript module augmentation
Alternatively, if you feel like using ClsService<MyClsStore>
everywhere is tedious, you can instead globally augment the ClsStore interface
.
Now you don't need to specify the type parameter on ClsService
to still get the string typing.
declare module 'nestjs-cls' {
interface ClsStore {
tenantId: string;
user: {
id: number;
authorized: boolean;
};
}
}
Using a custom provider
For even more transparent approach without augmenting the declaration, you can create a typed ClsService
by extending it and creating a custom provider out of it:
export class MyClsService extends ClsService<MyClsStore> {}
@Module({
imports: [ClsModule.forFeature()],
providers: [
{
provide: MyClsService,
useExisting: ClsService,
},
],
exports: [MyClsService],
})
class MyClsModule {}
Now you can inject MyClsService
as an alias for ClsService<MyClsStore>
without "polluting" the global type space.
Please note that in this case, the extended class acts only as an alternative Injection token. It doesn't allow you to extend the ClsService
with custom methods (they wouldn't be accessible on the injected instance, because what gets injected is the original ClsService
).
If you're thinking of extending the ClsService
, please consider instead wrapping it in a custom provider (an adapter/facade if you will) that exposes only methods appropriate for your application and proxies calls to ClsService
. If you still aren't convinced, please create a feature request and explain your reasoning.
Terminal Type
It can happen, that the object you want to store in the context is too complex, or contains cyclic references.
In that case, typescript might complain that type instantiation is too deep, possibly infinite. That is due to the fact that it tries to generate all possible paths inside the ClsStore
. If that's the case, you can use the Terminal
type to stop generating the paths for a certain subtree:
interface ClsStore {
tenantId: string;
user: Terminal<{
id: number;
authorized: boolean;
}>;
}
This will only generate the paths tenantId | user
and won't allow directly accessing nested keys (like cls.get('user.id')
, but you'll still get fully typing for things like const { id } = cls.get('user')
). See issue #22 for more details.