Clawject Type Systemโ
By default - Clawject type system follows a typescript type system as much as possible, but there is one major difference - TypeScript using structural type system (duck typing) but Clawject using nominal typing.
You can configure Clawject to use structural typing by setting typeSystem option to structural
in configuration file.
In a nutshell, nominal typing means that types are compared based on their name and place in the code, and structural typing means that types are compared based on their structure.
As an example - you have 2 classes Cat
and Dog
, both of them have the same properties and methods,
if you will use Cat
instead of Dog
or vice versa -
TypeScript will not complain about it, because both classes have the same structure.
class Cat {
name = 'cat';
voice(): void {
console.log('meow');
}
}
class Dog {
name = 'dog';
voice(): void {
console.log('woof');
}
}
class CatOwner {
constructor(private pet: Cat) {}
}
const dog = new Dog();
const catOwner = new CatOwner(dog); // <- TypeScript will not complain about it
But clawject will and report compile error about missing Bean declaration for Cat
:
class Cat {
name = 'cat';
voice(): void {
console.log('meow');
}
}
class Dog {
name = 'dog';
voice(): void {
console.log('woof');
}
}
class CatOwner {
constructor(private pet: Cat) {}
}
@ClawjectApplication
class Application {
dog = Bean(Dog);
catOwner = Bean(CatOwner); // <- Cannot find a Bean candidate for 'pet'.
}
Nominal type systemโ
All the following examples are based on the Clawjects nominal type system implementation, so if you're chosen structural type system - some examples may not work as expected.
Primitive typesโ
Clawject supports all primitive types like number
, string
, type literals, etc. from TypeScript, and compares them the same as typescript does.
Check Bean Types section for not supported bean types.
Object typesโ
Because Clawject is using nominal typing โ it's relying on at type declaration name and place.
class Foo {}
interface Bar {}
type Baz = {};
@ClawjectApplication
class Application {
@Bean foo: Foo = new Foo();
@Bean bar: Bar = {};
@Bean baz: Baz = {};
@PostConstruct
postConstruct(
dep0: Foo, // <- foo bean will be injected here
dep1: Bar, // <- bar bean will be injected here
dep2: Baz, // <- baz bean will be injected here
) {}
}
@ClawjectApplication
class Application {
@Bean foo: { bar: string } = {bar: 'barValue'};
@PostConstruct
postConstruct(
// compilation error will be reported here, because structurally these types are identical,
// but they have different declarations
foo: { bar: string },
) {}
}
Generic typesโ
Clawject has first-class support for generic types:
class Foo<T> {
constructor(
private data: T
) {}
}
@ClawjectApplication
class Application {
@Bean stringData = 'meow';
@Bean numberData = 42;
/* `stringData` bean will be injected for constructor parameter `data` */
fooWithString = Bean(Foo<string>);
/* `numberData` bean will be injected for constructor parameter `data` */
fooWithNumber = Bean(Foo<number>);
}
If generic is not defined explicitly, clawject will try to resolve it in the following way:
- If a generic type has a default value, it will be used as a fallback type.
- If a generic type has
extends
constraint - it will be used as a fallback type. - If you're not specifying a generic type explicitly, and it doesn't have default value or
extends
constraint - it will be treated asunknown
type (default typescript behavior).
Let's take a look at the next example to better understand how generic types are resolved when they are not specified explicitly:
class Foo<T> {
constructor(data: T) {}
}
class Bar<T = string> {
constructor(data: T) {}
}
class Baz {}
class Quux<T extends Baz> {
constructor(data: T) {}
}
@ClawjectApplication
class Application {
/* The type of parameter `data` will be `unknown` */
foo = Bean(Foo);
/* The type of parameter `data` will be `string` */
bar = Bean(Bar);
/* The type of parameter `data` will be `Baz` */
quux = Bean(Quux);
}
Classes and Interfaces inheritanceโ
When type is a class or interface type -
Clawject will automatically resolve a chain of type inheritance together with generics.
So, if you have class CacheImpl<T>
that implements ICache<T>
interface, and ICahce<T>
is extends IReadOnlyCache<T>
interface,
Clawject will treat CacheImpl<T>
as an intersection of CacheImpl<T>
, ICache<T>
and IReadOnlyCache<T>
.
import { Customer } from './customer';
import { Store } from './store';
interface IReadOnlyCache<T> {
get(key: string): T | null;
}
interface ICache<T> extends IReadOnlyCache<T> {
set(key: string, value: T): void;
clear(): void;
}
class CacheImpl<T> implements ICache<T> {
/* ... */
}
class CustomerService {
constructor(
/* Clawject injects "customerCache" bean just by interface with a generic type */
private cache: IReadOnlyCache<Customer>
) {}
}
class StoreService {
constructor(
/* Clawject injects "storeCache" bean just by interface with a generic type */
private cache: ICache<Store>
) {}
}
class CacheManager {
constructor(
/* Clawject injects array of all found beans with type ICache (customerCache, storeCache) */
private caches: ICache<any>[]
) {}
}
@ClawjectApplication
class Application {
customerCache = Bean(CacheImpl<Customer>);
storeCache = Bean(CacheImpl<Store>);
customerService = Bean(CustomerService);
storeService = Bean(StoreService);
cacheManager = Bean(CacheManager);
}
Intersection typesโ
Clawject fully supports intersection types as bean types, and as bean dependency types:
interface IFoo { foo: string }
interface IBar { bar: string }
interface IBaz { baz: string }
@ClawjectApplication
class Application {
@Bean fooAndBarAndBaz: IFoo & IBar & IBaz = { foo: 'fooValue', bar: 'barValue', baz: 'bazValue' };
@PostConstruct
postConstruct(
dep0: IFoo, // <- "fooAndBarAndBaz" will be injected
dep1: IBar, // <- "fooAndBarAndBaz" will be injected
dep2: IBar, // <- "fooAndBarAndBaz" will be injected
dep3: IFoo & IBar, // <- "fooAndBarAndBaz" will be injected
dep4: IFoo & IBaz, // <- "fooAndBarAndBaz" will be injected
dep5: IFoo & IBar & IBaz, // <- "fooAndBarAndBaz" will be injected
) {}
}
Clawject also can resolve complex generic types as a dependencies:
class Repository<T> {}
class Service<T> {
constructor(
private repository: Repository<T>,
) {}
}
interface Foo { foo: string }
interface Bar { bar: string }
@ClawjectApplication
class Application {
fooRepository = Bean(Repository<Foo>);
barRepository = Bean(Repository<Bar>);
fooService = Bean(Service<Foo>) // <- "fooRepository" will be injected as a "repository" dependency
@PostConstruct
postConstruct(
service: Service<Foo>, // <- "fooService" will be injected
) {}
}
Union typesโ
Clawject supports union types only as a bean dependency types, so it's not possible to create a bean with a union type, but it's possible to request a bean using a union type:
interface IFoo { foo: string }
interface IBar { bar: string }
@ClawjectApplication
class Application {
@Bean bar: IBar = { bar: 'barValue' }
@PostConstruct
postConstruct(
dep0: IFoo | IBar, // <- "bar" will be injected here, because IFoo was not declared as a bean
) {}
}
Tuple typesโ
Clawject has support for tuple types.
Tuple types are treated as a nominal types:
@ClawjectApplication
class Application {
@Bean tuple: [string, number] = ['foo', 42];
@PostConstruct
postConstruct(
dep0: [string, number], // <- "tuple" bean will be injected here
) {}
}
Type aliasesโ
From the typescript point of view -
type aliases are just a named wrapper around base types like string
, number
, object literals, etc.
So type aliases will be treated as a set of base types.
interface Foo {}
interface Bar {}
type Baz = Foo & Bar;
@ClawjectApplication
class Application {
/* Type of Bean resolved to `Foo` */
@Bean foo: Foo = {};
/* Type of Bean resolved to `Bar` */
@Bean bar: Bar = {};
/* Type of Bean resolved to `Foo & Bar` */
@Bean baz: Baz = {};
/* Type is identical to `baz` Bean */
@Bean fooAndBar: Foo & Bar = {};
}