Skip to main content

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.

tip

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.

tip

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.

Using type references
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
) {}
}
Using object literals
@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 as unknown 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 = {};
}