Skip to content

Instantly share code, notes, and snippets.

@alxhub
Last active May 29, 2018 22:44
Show Gist options
  • Save alxhub/6ffdf23ca62c3a182fb3d1138603d542 to your computer and use it in GitHub Desktop.
Save alxhub/6ffdf23ca62c3a182fb3d1138603d542 to your computer and use it in GitHub Desktop.

Implementation plan for Inheritance in Ivy

In View Engine code, components and directives can inherit from base classes which also have Angular decorators. Data passed to annotations for the decorators will inherit properly between base and derived classes. This works as the metadata/summary system retains enough global information to resolve inheritance structures at compile time.

Consider the following structure:

@Directive({...})
export class Base {
  constructor(readonly dep: Dep) {}
}

@Directive({...})
export class Derived extends Base {}

Any process which attempts to instantiate Derived must somehow deal with the fact that the constructor parameter types are defined on Base, which could come from a different library or compilation unit. The goal is to generate a function that constructs Derived by injecting its dependencies:

factory: () => new Derived(inject(Dep)),

To support this, it's essential to generate Base in such a way that its factory can be used to construct Derived. The proposed generated code is:

import {delegateDirectiveCtor} from '@angular/core';

export class Base {
  static ngDirectiveDef = defineDirective({
    factory: (derived?) => new (derived || Base)(inject(Dep)),
  });
}

export class Derived extends Base {
  static ngDirectiveDef = defineDirective({
    factory: (derived?) => delegateDirectiveCtor(Base, derived || Derived),
  });
}

When Base.factory() is called directly, a new instance of Base is created. When Derived.factory() is called, it invokes Base.factory(Derived) and a new instance of Derived is constructed using Base's constructor injection.


Why delegateDirectiveCtor?

The runtime shape of ngDirectiveDef objects is private API, and generated code cannot directly access it. A function wrapper is used to construct the Derived class using the base class's factory function.


Deep inheritance is also supported by having Derived's factory function also take a derived type argument.

For @Injectable, it's a bit trickier. @Injectables have factory functions which don't always instantiate the given class. For example, an @Injectable with useFactory and separate deps will declare a factory function as follows:

@Injectable({
  useFactory: makeBase,
  deps: [FactoryDep],
})
export class Base {
  constructor(readonly dep: Dep) {}

  static ngInjectableDef = defineInjectable({
    factory: (derived?) => derived ? new Derived(inject(Dep)) : makeBase(inject(FactoryDep)),
  });
}

In the case where a parameter of an @Injectable constructor is unresolvable (for example, the factory function generated will call unresolvable() instead of inject for that parameter, which will throw a helpful error indicating that the parameter could not be resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment