Cannot autoinject abstract classes in aurelia-dependency-injection 1.5.0

A recent update to the dependency injection makes the autoinject (and i guess others) decorator incompatible with TypeScript abstract classes.
This breaks an inheritance-based scenario common in our application:

  • we use abstract, generic classes for viewModels with high-level shared functionality (e.g. a generic form control, a generic detail page…). These use @autoinject and have a defined constructor with a sometimes large number of arguments: for reference the generic detail page constructor currently has 9 arguments, all services (Aurelia’s or ours) like EventAggregator, Router and BindingSignaler
  • the concrete viewModels inherit (extends) from these abstract base viewModels. Most of the time there is no need to override the default constructor as it covers everything, so the concrete viewModel has no explicitly defined constructor and no @autoinject.

We have currently downgraded to 1.4.2 (and aurelia-validation to 1.4.0), but this is obviously not a long-term solution.

To fix this problem in our application, we could:

  • make the abstract classes not abstract. This would cause no concrete issues but defeats the semantic purpose of using abstract in the first place.
  • move @autoinject to the last level of concrete classes. This is arguably more semantically correct, but most of the time it would be redundant and prone to being forgotten (in the transition stage, at least).

However, before rushing to change such a large number of files I would appreciate to know if our structure is sound enough, and if so if it is enough of a common / useful use case to warrant some kind of consideration in the aurelia-dependency-injection type definitions.

If this is relevant, we currently have to target ES5.


Furthermore, with 1.5.0 we get this error while compiling:
…/node_modules/aurelia-dependency-injection/dist/aurelia-dependency-injection.d.ts(139,162): error TS2536: Type ‘number’ cannot be used to index type ‘TArgs’.
This could be related to our version of TypeScript, however (I am decently sure it is 3.1 which is quite outdated).

4 Likes

For cases like these I tend to not depend on @autoinject but instead manually @inject. Is that an option?

1 Like

Looks like an oversight in that change. The constraint (new() => any) was added in order to limit the decorator to newable types, but abstract classes are not newable.

We may need to loosen up the type constraints again. See https://github.com/aurelia/dependency-injection/pull/182

1 Like

It could be, though we’d lose the terseness of @autoinject which has always served us well.
Does @inject have any relevant advantages over @autoinject? Note that we currently do not directly use more complex DI feature like NewInstance.of.

1 Like

@m-gallesio I verified @autoinject works at typescript@3.4.1

1 Like

Thank you for your feedback, I will check whenever possible (our project at the moment has higher-priority issues).

1 Like

Unfortunately it still does not seem to work, even with later TypeScript version (I have tried installing 3.5.2).

Here is a minimal project (created with Visual Studio 2017’s node.js application template) which should reproduce the issue:

package.json:

{
  "name": "tstest",
  "version": "0.0.0",
  "description": "TSTest",
  "main": "app.js",
  "author": {
    "name": ""
  },
  "devDependencies": {
    "@types/node": "^8.0.14"
  },
  "dependencies": {
    "aurelia-dependency-injection": "^1.5.0",
    "aurelia-framework": "^1.3.1",
    "aurelia-validation": "^1.5.0"
  }
}

tsconfig.json:

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es5",
    "lib": ["es6"],
    "sourceMap": true,
    "experimentalDecorators": true
  },
  "exclude": [
    "node_modules"
  ]
}

app.ts:

import { autoinject } from "aurelia-framework"
@autoinject
export abstract class Test {
}

No matter what, the TypeScript compiler complains with:

error TS2345: Argument of type 'typeof Test' is not assignable to parameter of type 'DependencyCtor<any, any, any>'.
Cannot assign an abstract constructor type to a non-abstract constructor type.
2 Likes

This is happening again in the current version of Aurelia 2 (2.0.0-alpha.2).
From what I understand autoinject has been conflated with inject.

This:

@inject()
export abstract class IndexPage<TItem> implements IRouteViewModel {
}

fails with the following error:

Argument of type 'typeof IndexPage' is not assignable to parameter of type 'Injectable<{}>'.
  Type 'typeof IndexPage' is not assignable to type 'Constructable<{}>'.
    Cannot assign an abstract constructor type to a non-abstract constructor type.ts(2345)

I seem to recall some recent version of Typescript doing (or planning to do) something with abstract constructors; it might be worth looking into.

2 Likes

Ahh interesting, well maybe we will loosen it in v2 like we did in v1 :+1:

They enabled Abstract class argument inference.

export type AbsCtorArgs<TBase> = TBase extends abstract new (...args: infer TArgs) => infer Impl ? TArgs : never;