-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(strongly-typed): initial commit
Add an initial offering for `@inversifyjs/strongly-typed`, which will let consumers optionally apply strong typing to their `Container`.
- Loading branch information
1 parent
a82f20e
commit e80b1d3
Showing
16 changed files
with
608 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# Typescript compiled files | ||
/lib/** | ||
|
||
/tsconfig.tsbuildinfo | ||
/tsconfig.cjs.tsbuildinfo | ||
/tsconfig.esm.tsbuildinfo | ||
|
||
# Test coverage report | ||
/coverage | ||
|
||
# Test mutation report | ||
/reports | ||
|
||
# node modules | ||
/node_modules/ | ||
|
||
# Turborepo files | ||
.turbo/ | ||
|
9 changes: 9 additions & 0 deletions
9
packages/container/libraries/strongly-typed/.lintstagedrc.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"*.js": [ | ||
"prettier --write" | ||
], | ||
"*.ts": [ | ||
"prettier --write", | ||
"eslint" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
**/*.spec.js | ||
**/*.spec.js.map | ||
**/*.ts | ||
!lib/**/*.d.ts | ||
lib/**/*.spec.d.ts | ||
|
||
.lintstagedrc.json | ||
eslint.config.mjs | ||
jest.config.mjs | ||
jest.config.stryker.mjs | ||
jest.js.config.mjs | ||
prettier.config.mjs | ||
stryker.config.mjs | ||
tsconfig.json | ||
tsconfig.cjs.json | ||
tsconfig.esm.json | ||
tsconfig.tsbuildinfo |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# @inversifyjs/strongly-typed |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
[![Test coverage](https://codecov.io/gh/inversify/monorepo/branch/main/graph/badge.svg?flag=%40inversifyjs%2Fstrongly-typed)](https://codecov.io/gh/inversify/monorepo/branch/main/graph/badge.svg?flag=%40inversifyjs%2Fstrongly-typed) | ||
[![npm version](https://img.shields.io/github/package-json/v/inversify/monorepo?filename=packages%2Fcontainer%2Flibraries%2Fstrongly-typed%2Fpackage.json&style=plastic)](https://www.npmjs.com/package/@inversifyjs/strongly-typed) | ||
|
||
# @inversifyjs/strongly-typed | ||
|
||
Type definitions for adding strong typing to the `Container` and `@inject()` decorator. | ||
|
||
## Usage | ||
|
||
This library can be used in a couple of ways, depending on your preference. | ||
|
||
All usages wind up with you having a strongly-typed `TypedContainer`, which is a generic class that accepts a binding map as a type argument, which forms the contract that your container will adhere to: | ||
|
||
```ts | ||
import { TypedContainer } from '@inversifyjs/strongly-typed'; | ||
|
||
interface Foo { foo: string } | ||
interface Bar { bar: string } | ||
|
||
interface BindingMap { | ||
foo: Foo; | ||
bar: Bar; | ||
} | ||
|
||
const container = new TypedContainer<BindingMap>(); | ||
|
||
// Bindings are now strongly typed: | ||
|
||
container.bind('foo').toConstantValue({foo: 'abc'}); // ok | ||
container.rebind('foo').toConstantValue({unknown: 'uh-oh'}) // compilation error | ||
|
||
let foo: Foo = container.get('foo') // ok | ||
foo = container.get('bar') // compilation error | ||
foo = container.get('unknown-identifier') // compilation error | ||
``` | ||
|
||
### Instantiation | ||
|
||
The simplest way to use the library is to directly construct a `TypedContainer`: | ||
|
||
```ts | ||
import { TypedContainer } from '@inversifyjs/strongly-typed'; | ||
|
||
const container = new TypedContainer<BindingMap>(); | ||
``` | ||
|
||
This class is actually just a re-typed re-export of the vanilla `Container`, so shares all underlying functionality. | ||
|
||
### Type assertion | ||
|
||
If you'd prefer to keep this library out of your final dependency tree, you can just import the types and perform a type assertion: | ||
|
||
```ts | ||
import { Container } from 'inversify'; | ||
// The type import will be stripped during transpilation | ||
import type { TypedContainer } from '@inversifyjs/strongly-typed'; | ||
|
||
const container = new Container() as TypedContainer<BindingMap>; | ||
``` |
3 changes: 3 additions & 0 deletions
3
packages/container/libraries/strongly-typed/eslint.config.mjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import myconfig from '@inversifyjs/foundation-eslint-config'; | ||
|
||
export default [...myconfig]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { tsGlobalConfig } from '@inversifyjs/foundation-jest-config'; | ||
|
||
export default tsGlobalConfig; |
3 changes: 3 additions & 0 deletions
3
packages/container/libraries/strongly-typed/jest.js.config.mjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { jsGlobalConfig } from '@inversifyjs/foundation-jest-config'; | ||
|
||
export default jsGlobalConfig; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
{ | ||
"author": "Alec Gibson", | ||
"bugs": { | ||
"url": "https://github.com/inversify/monorepo/issues" | ||
}, | ||
"description": "InversifyJs strong type definitions", | ||
"devDependencies": { | ||
"@eslint/js": "9.13.0", | ||
"@jest/globals": "29.7.0", | ||
"@types/node": "20.17.4", | ||
"@typescript-eslint/eslint-plugin": "8.12.2", | ||
"@typescript-eslint/parser": "8.12.2", | ||
"eslint": "9.13.0", | ||
"jest": "29.7.0", | ||
"prettier": "3.3.3", | ||
"ts-jest": "29.2.5", | ||
"typescript": "5.6.3" | ||
}, | ||
"devEngines": { | ||
"node": "^20.18.0", | ||
"pnpm": "^9.12.1" | ||
}, | ||
"homepage": "https://inversify.io", | ||
"keywords": [ | ||
"dependency injection", | ||
"dependency inversion", | ||
"di", | ||
"inversion of control container", | ||
"ioc", | ||
"javascript", | ||
"node", | ||
"typescript" | ||
], | ||
"license": "MIT", | ||
"main": "lib/cjs/index.js", | ||
"module": "lib/esm/index.js", | ||
"exports": { | ||
".": { | ||
"import": "./lib/esm/index.js", | ||
"require": "./lib/cjs/index.js" | ||
} | ||
}, | ||
"name": "@inversifyjs/strongly-typed", | ||
"os": [ | ||
"darwin", | ||
"linux" | ||
], | ||
"peerDependencies": { | ||
"inversify": ">=6.0.3" | ||
}, | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/inversify/monorepo.git" | ||
}, | ||
"scripts": { | ||
"build": "pnpm run build:cjs && pnpm run build:esm", | ||
"build:cjs": "tsc --build tsconfig.cjs.json && pnpm exec foundation-ts-package-cjs ./lib/cjs", | ||
"build:esm": "tsc --build tsconfig.esm.json && pnpm exec foundation-ts-package-esm ./lib/esm", | ||
"build:clean": "rimraf lib", | ||
"format": "prettier --write ./src/**/*.ts", | ||
"lint": "eslint ./src", | ||
"test": "jest --config=jest.config.mjs --runInBand", | ||
"test:js": "jest --config=jest.js.config.mjs --runInBand", | ||
"test:js:coverage": "pnpm run test:unit:js --coverage", | ||
"test:uncommitted": "pnpm run test --changedSince=HEAD", | ||
"test:unit:js": "pnpm run test:js --selectProjects Unit" | ||
}, | ||
"sideEffects": false, | ||
"version": "1.0.0" | ||
} |
3 changes: 3 additions & 0 deletions
3
packages/container/libraries/strongly-typed/prettier.config.mjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import config from '@inversifyjs/foundation-prettier-config'; | ||
|
||
export default config; |
183 changes: 183 additions & 0 deletions
183
packages/container/libraries/strongly-typed/src/container.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
/* eslint-disable @typescript-eslint/no-non-null-assertion */ | ||
/* eslint-disable jest/expect-expect */ | ||
import { beforeEach, describe, expect, it } from '@jest/globals'; | ||
|
||
import 'reflect-metadata'; | ||
|
||
import { Container, injectable } from 'inversify'; | ||
|
||
import { TypedContainer } from './index'; | ||
|
||
describe('interfaces', () => { | ||
@injectable() | ||
class Foo { | ||
public foo: string = ''; | ||
} | ||
|
||
@injectable() | ||
class Bar { | ||
public bar: string = ''; | ||
} | ||
|
||
describe('Container', () => { | ||
let foo: Foo; | ||
let foos: Foo[]; | ||
|
||
beforeEach(() => { | ||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions | ||
foo; | ||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions | ||
foos; | ||
}); | ||
|
||
describe('no binding map', () => { | ||
let container: TypedContainer; | ||
|
||
beforeEach(() => { | ||
container = new Container() as TypedContainer; | ||
}); | ||
|
||
describe('bind()', () => { | ||
it('binds without a type argument', () => { | ||
container.bind('foo').to(Foo); | ||
container.bind(Foo).to(Foo); | ||
}); | ||
|
||
it('checks bindings with an explicit type argument', () => { | ||
container.bind<Foo>('foo').to(Foo); | ||
// @ts-expect-error :: can't bind Bar to Foo | ||
container.bind<Foo>('foo').to(Bar); | ||
}); | ||
|
||
it('binds a class as a service identifier', () => { | ||
container.bind(Foo).to(Foo); | ||
// @ts-expect-error :: can't bind Bar to Foo | ||
container.bind(Foo).to(Bar); | ||
}); | ||
}); | ||
|
||
describe('get()', () => { | ||
beforeEach(() => { | ||
container.bind('foo').to(Foo); | ||
container.bind('bar').to(Bar); | ||
container.bind(Foo).to(Foo); | ||
container.bind(Bar).to(Bar); | ||
}); | ||
|
||
it('gets an anonymous binding', () => { | ||
foo = container.get('foo'); | ||
}); | ||
|
||
it('enforces type arguments', () => { | ||
foo = container.get<Foo>('foo'); | ||
// @ts-expect-error :: can't assign Bar to Foo | ||
foo = container.get<Bar>('bar'); | ||
}); | ||
|
||
it('gets a class identifier', () => { | ||
foo = container.get(Foo); | ||
// @ts-expect-error :: can't assign Bar to Foo | ||
foo = container.get(Bar); | ||
}); | ||
|
||
it('gets all', () => { | ||
foos = container.getAll<Foo>('foo'); | ||
// @ts-expect-error :: can't assign Bar to Foo | ||
foos = container.getAll<Bar>('bar'); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('binding map', () => { | ||
interface BindingMap { | ||
foo: Foo; | ||
bar: Bar; | ||
asyncNumber: Promise<number>; | ||
} | ||
|
||
let container: TypedContainer<BindingMap>; | ||
|
||
beforeEach(() => { | ||
container = new Container() as TypedContainer<BindingMap>; | ||
}); | ||
|
||
describe('bind()', () => { | ||
it('enforces strict bindings', () => { | ||
container.bind('foo').to(Foo); | ||
// @ts-expect-error :: can't bind Bar to Foo | ||
container.bind('foo').to(Bar); | ||
// @ts-expect-error :: unknown service identifier | ||
container.bind('unknown').to(Foo); | ||
}); | ||
}); | ||
|
||
describe('get()', () => { | ||
beforeEach(() => { | ||
container.bind('foo').to(Foo); | ||
container.bind('bar').to(Bar); | ||
container.bind('asyncNumber').toConstantValue(Promise.resolve(1)); | ||
}); | ||
|
||
it('gets a promise', async () => { | ||
// @ts-expect-error :: can't call get() to get Promise | ||
expect(() => container.get('asyncNumber')).toThrow( | ||
'it has asynchronous dependencies', | ||
); | ||
const n: number = await container.getAsync('asyncNumber'); | ||
expect(n).toBe(1); | ||
}); | ||
|
||
it('enforces strict bindings', () => { | ||
foo = container.get('foo'); | ||
// @ts-expect-error :: can't assign Bar to Foo | ||
foo = container.get('bar'); | ||
// @ts-expect-error :: unknown service identifier | ||
expect(() => container.get('unknown') as unknown).toThrow( | ||
'No matching bindings', | ||
); | ||
}); | ||
|
||
it('gets all', () => { | ||
foos = container.getAll('foo'); | ||
// @ts-expect-error :: can't assign Bar to Foo | ||
foos = container.getAll('bar'); | ||
}); | ||
}); | ||
|
||
describe('ancestry', () => { | ||
beforeEach(() => { | ||
container.bind('foo').to(Foo); | ||
container.bind('bar').to(Bar); | ||
}); | ||
|
||
it('tracks the types of ancestors', () => { | ||
// eslint-disable-next-line @typescript-eslint/typedef | ||
const child = container.createChild<{ lorem: string }>(); | ||
child.bind('lorem').toConstantValue('lorem'); | ||
foo = child.parent!.get('foo'); | ||
// @ts-expect-error :: can't assign Bar to Foo | ||
foo = child.parent!.get('bar'); | ||
|
||
// eslint-disable-next-line @typescript-eslint/typedef | ||
const grandchild = child.createChild<{ ipsum: string }>(); | ||
const lorem: string = grandchild.parent!.get('lorem'); | ||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions | ||
lorem; | ||
|
||
foo = grandchild.parent!.parent!.get('foo'); | ||
// @ts-expect-error :: can't assign Bar to Foo | ||
foo = grandchild.parent!.parent!.get('bar'); | ||
}); | ||
}); | ||
|
||
describe('instantiation', () => { | ||
it('constructs', () => { | ||
container = new TypedContainer<BindingMap>(); | ||
container.bind('foo').to(Foo); | ||
// @ts-expect-error :: can't bind Bar to Foo | ||
container.bind('foo').to(Bar); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.