Skip to content
This repository was archived by the owner on Apr 21, 2024. It is now read-only.

Commit 568554b

Browse files
authored
Merge pull request #15 from Coder-Spirit/fix-register-constructor-type
Introduce cc2ic to allow injecting "class constructors" as "interface constructors"
2 parents 8408706 + 6c4402a commit 568554b

File tree

10 files changed

+337
-241
lines changed

10 files changed

+337
-241
lines changed

.github/workflows/tests.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818

1919
strategy:
2020
matrix:
21-
node-version: [14.x, 16.x, 17.x]
21+
node-version: [14.x, '16.4.0', 17.x]
2222

2323
steps:
2424
- uses: actions/checkout@v2

README.md

+12-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { ... } from 'https://deno.land/x/lambda_ioc@[VERSION]/lambda-ioc/deno/in
3737

3838
```ts
3939
import {
40+
cc2ic, // Stands for "class-constructor to interface-constructor"
4041
constructor,
4142
createContainer,
4243
func
@@ -46,7 +47,12 @@ function printNameAndAge(name: string, age: number) {
4647
console.log(`${name} is aged ${age}`)
4748
}
4849

49-
class Person {
50+
interface Human {
51+
age: number
52+
name: readonly string
53+
}
54+
55+
class Person implements Human {
5056
constructor(
5157
public readonly age: number,
5258
public readonly name: string
@@ -60,6 +66,11 @@ const container = createContainer()
6066
.register('fn', func(printNameAndAge, 'someName', 'someAge'))
6167
// And constructors too
6268
.register('Person', constructor(Person, 'someAge', 'someName'))
69+
// We can do that directly, without having import `constructor`:
70+
.registerConstructor('AnotherPerson', Person, 'someAge', 'someName')
71+
// In case we want to register a "concrete" constructor to provide an
72+
// abstract interface, we'll have to apply a small hack, using `cc2ic`:
73+
.registerConstructor('Human', cc2ic<Human>()(Person), 'someAge', 'someName')
6374
// We can "define groups" by using `:` as an infix, the group's name will be
6475
// the first part of the string before `:`.
6576
// Groups can be used in all "register" methods.

lambda-ioc/README.md

+12-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { ... } from 'https://deno.land/x/lambda_ioc@[VERSION]/lambda-ioc/deno/in
3737

3838
```ts
3939
import {
40+
cc2ic, // Stands for "class-constructor to interface-constructor"
4041
constructor,
4142
createContainer,
4243
func
@@ -46,7 +47,12 @@ function printNameAndAge(name: string, age: number) {
4647
console.log(`${name} is aged ${age}`)
4748
}
4849

49-
class Person {
50+
interface Human {
51+
age: number
52+
name: readonly string
53+
}
54+
55+
class Person implements Human {
5056
constructor(
5157
public readonly age: number,
5258
public readonly name: string
@@ -60,6 +66,11 @@ const container = createContainer()
6066
.register('fn', func(printNameAndAge, 'someName', 'someAge'))
6167
// And constructors too
6268
.register('Person', constructor(Person, 'someAge', 'someName'))
69+
// We can do that directly, without having import `constructor`:
70+
.registerConstructor('AnotherPerson', Person, 'someAge', 'someName')
71+
// In case we want to register a "concrete" constructor to provide an
72+
// abstract interface, we'll have to apply a small hack, using `cc2ic`:
73+
.registerConstructor('Human', cc2ic<Human>()(Person), 'someAge', 'someName')
6374
// We can "define groups" by using `:` as an infix, the group's name will be
6475
// the first part of the string before `:`.
6576
// Groups can be used in all "register" methods.

lambda-ioc/deno/combinators.ts

+28
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,15 @@ export function constructor<
113113
}
114114
}
115115

116+
// Class-Constructor to Interface-Constructor
117+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
118+
export function cc2ic<I>(): <CC extends new (...args: any[]) => I>(cc: CC) => AsInterfaceCtor<I, CC> {
119+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
120+
return identity as <C extends I, CC extends new (...args: any[]) => C>(
121+
cc: CC,
122+
) => AsInterfaceCtor<I, CC>
123+
}
124+
116125
// -----------------------------------------------------------------------------
117126
// Private Types
118127
// -----------------------------------------------------------------------------
@@ -125,3 +134,22 @@ type SyncFuncContainer<
125134
Extract<Zip<TSyncDependencies, TParams>, readonly [ContainerKey, any][]>
126135
>
127136
>
137+
138+
// Converts a "class constructor" type to an "interface constructor" type
139+
type AsInterfaceCtor<
140+
I, // The interface we are interested on
141+
// We have to pass CC to know its constructor parameters
142+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
143+
CC extends new (...args: any[]) => I = new () => I,
144+
> = CC extends new (...args: infer A) => infer C
145+
? C extends I
146+
? new (...args: A) => I
147+
: never
148+
: never
149+
150+
// -----------------------------------------------------------------------------
151+
// Private Functions
152+
// -----------------------------------------------------------------------------
153+
154+
// Helper function, here to avoid creating too many anonymous lambdas
155+
const identity = (x: unknown) => x

lambda-ioc/deno/index.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,10 @@ export {
88
type WritableContainer,
99
createContainer,
1010
} from './container.ts';
11-
export { asyncSingleton, constructor, func, singleton } from './combinators.ts';
11+
export {
12+
asyncSingleton,
13+
cc2ic,
14+
constructor,
15+
func,
16+
singleton,
17+
} from './combinators.ts';

lambda-ioc/package.json

+10-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@coderspirit/lambda-ioc",
3-
"version": "0.7.1",
3+
"version": "0.8.0",
44
"main": "./dist/cjs/index.js",
55
"module": "./dist/esm/index.js",
66
"types": "./dist/cjs/index.d.ts",
@@ -40,17 +40,17 @@
4040
"devDependencies": {
4141
"@types/jest": "^27.4.1",
4242
"@types/node": "^14.14.37",
43-
"@typescript-eslint/eslint-plugin": "^5.12.1",
44-
"@typescript-eslint/parser": "^5.12.1",
45-
"eslint": "^8.10.0",
46-
"eslint-config-prettier": "^8.4.0",
47-
"eslint-plugin-jest": "^26.1.1",
43+
"@typescript-eslint/eslint-plugin": "^5.18.0",
44+
"@typescript-eslint/parser": "^5.18.0",
45+
"eslint": "^8.13.0",
46+
"eslint-config-prettier": "^8.5.0",
47+
"eslint-plugin-jest": "^26.1.4",
4848
"eslint-plugin-node": "^11.1.0",
4949
"jest": "^27.5.1",
50-
"prettier": "^2.5.1",
51-
"ts-jest": "^27.1.3",
52-
"ts-node": "^10.5.0",
53-
"typescript": "^4.5.5"
50+
"prettier": "^2.6.2",
51+
"ts-jest": "^27.1.4",
52+
"ts-node": "^10.7.0",
53+
"typescript": "^4.6.3"
5454
},
5555
"engines": {
5656
"node": ">=14.0.0"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { cc2ic } from '../combinators'
2+
import { createContainer } from '..'
3+
4+
interface IA {
5+
f: () => string
6+
}
7+
8+
class A implements IA {
9+
constructor(private readonly s: string) {}
10+
public f(): string {
11+
return this.s
12+
}
13+
}
14+
15+
class B {
16+
constructor(private readonly a: IA) {}
17+
public g(): IA {
18+
return this.a
19+
}
20+
}
21+
22+
describe('container regressions', () => {
23+
describe('registerConstructor', () => {
24+
it('allows to register constructors they are depended upon through interfaces', () => {
25+
const container = createContainer()
26+
.registerValue('greeting', 'hello')
27+
.registerConstructor('A', cc2ic<IA>()(A), 'greeting') // We have to rely on cc2ic
28+
.registerConstructor('B', B, 'A')
29+
30+
expect(container.resolve('B').g().f()).toBe('hello')
31+
})
32+
})
33+
})

lambda-ioc/src/combinators.ts

+28
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,15 @@ export function constructor<
113113
}
114114
}
115115

116+
// Class-Constructor to Interface-Constructor
117+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
118+
export function cc2ic<I>(): <CC extends new (...args: any[]) => I>(cc: CC) => AsInterfaceCtor<I, CC> {
119+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
120+
return identity as <C extends I, CC extends new (...args: any[]) => C>(
121+
cc: CC,
122+
) => AsInterfaceCtor<I, CC>
123+
}
124+
116125
// -----------------------------------------------------------------------------
117126
// Private Types
118127
// -----------------------------------------------------------------------------
@@ -125,3 +134,22 @@ type SyncFuncContainer<
125134
Extract<Zip<TSyncDependencies, TParams>, readonly [ContainerKey, any][]>
126135
>
127136
>
137+
138+
// Converts a "class constructor" type to an "interface constructor" type
139+
type AsInterfaceCtor<
140+
I, // The interface we are interested on
141+
// We have to pass CC to know its constructor parameters
142+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
143+
CC extends new (...args: any[]) => I = new () => I,
144+
> = CC extends new (...args: infer A) => infer C
145+
? C extends I
146+
? new (...args: A) => I
147+
: never
148+
: never
149+
150+
// -----------------------------------------------------------------------------
151+
// Private Functions
152+
// -----------------------------------------------------------------------------
153+
154+
// Helper function, here to avoid creating too many anonymous lambdas
155+
const identity = (x: unknown) => x

lambda-ioc/src/index.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,10 @@ export {
88
type WritableContainer,
99
createContainer,
1010
} from './container'
11-
export { asyncSingleton, constructor, func, singleton } from './combinators'
11+
export {
12+
asyncSingleton,
13+
cc2ic,
14+
constructor,
15+
func,
16+
singleton,
17+
} from './combinators'

0 commit comments

Comments
 (0)