Issue
Using Typescript to write an Angular 5 unit tests, I have a function that will query the DOM and return an instance of the MyComponent
class if found:
function getMyComponent(hostFixture: ComponentFixture<any>): MyComponent {
const debugElement = hostFixture.debugElement.query(By.directive(MyComponent));
return (debugElement && debugElement.componentInstance) || null;
}
This works flawlessly. Now I'd like to generalize the function so that it could query any Type on demand. I figured this could be done with generic typesThe function would be called like so:
const instance: MyComponent = getDirective<MyComponent>(fixture);
I can't get it working though; Here is the broken function:
function getDirective<T>(hostFixture: ComponentFixture<any>): T {
const debugElement = hostFixture.debugElement.query(By.directive(T));
return (debugElement && debugElement.componentInstance) || null;
}
The first line within the function throws the error:
TS2693: 'T' only refers to a type, but is being used as a value here
What is the right syntax here? The Angular function I'm calling -- By.directive()
-- takes an argument of type Type<any>
, and is documented here
Solution
Angular's Type<T>
is a bad API. You are one of many who has been mislead by its frankly irresponsible naming (see remarks). Type<T>
really means Constructable. It is the type of a value that you can call with new
.
By.directive
takes a value not a type. The type of that value is something which can be called with new
, something that can be constructed. This means a class or a function.
However, you can write the function you wish to.
Here is how I would write it.
function getDirective<T>(
hostFixture: ComponentFixture<any>,
ComponentConstructor: new (...args: unknown[]) => T
): T {
const debugElement = hostFixture.debugElement.query(By.directive(ComponentConstructor));
return debugElement && debugElement.componentInstance || undefined;
}
Then call it like this
const instance = getDirective(fixture, MyComponent);
Remarks:
TypeScript types are purely design time constructs. The language's entire type system was carefully designed to be fully erased during transpilation and in particular to have no impact on the runtime behavior of programs.
Why do I say that the name Type<T>
is irresponsible? Because thousands of programmers are learning TypeScript for the first time in the context of Angular. By naming a type Type, they have made things very easy to misunderstand.
This is especially significant because the type Type<T>
is usually ascribed to a value that is a class and many people already confuse types with classes in TypeScript and in vanilla JavaScript as well.
Angular wants these concepts to be conflated, it wishes that classes in JavaScript were types like they are in many other languages but they simply are not. It's worth noting that frameworks such as Aurelia, which I prefer, also encourage this conflating but, since they do not attempt to mandate TypeScript and as they allow dependencies to be specified via a property on classes (perhaps ironically in the manner or AngularJS, confusion is less likely. Irregardless, TypeScript types, ECMAScript classes, and dependency injection are entirely orthogonal concepts.
Given the nature of TypeScript, which provides design time only types, this leads to serious confusion.
It is interesting to note that the Angular team designed their own programming language, called @Script specifically for the development of Angular 2. @Script was a derivative of JavaScript and TypeScript that distinguished itself by adding some degree of type reification while eschewing Decorators for a then competing Annotations feature. They abandoned this language late in 2014, opting to use TypeScript instead.
Still I sometimes think that the same philosophy that inspired @Script still influences Angular.
Answered By - Aluan Haddad
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.