refactor(compiler-cli): remove event output helper from TCB (#40738)
In 5c547675b11a24b16c20df1718583a0e7ed49cbd the `EventEmitter.subscribe` API was extended with a new signature that allows the emitter's generic type `T` to flow into the subscribe callback. This new signature removes the need for the special `_outputHelper` function that used to be emitted into TCBs when `strictOutputEventTypes`/`strictTemplates` is enabled. PR Close #40738
This commit is contained in:
parent
e895166082
commit
94f4d5cba6
|
@ -41,9 +41,6 @@ export class Environment {
|
|||
private pipeInsts = new Map<ClassDeclaration, ts.Expression>();
|
||||
protected pipeInstStatements: ts.Statement[] = [];
|
||||
|
||||
private outputHelperIdent: ts.Identifier|null = null;
|
||||
protected helperStatements: ts.Statement[] = [];
|
||||
|
||||
constructor(
|
||||
readonly config: TypeCheckingConfig, protected importManager: ImportManager,
|
||||
private refEmitter: ReferenceEmitter, private reflector: ReflectionHost,
|
||||
|
@ -113,93 +110,6 @@ export class Environment {
|
|||
return pipeInstId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Declares a helper function to be able to cast directive outputs of type `EventEmitter<T>` to
|
||||
* have an accurate `subscribe()` method that properly carries over the generic type `T` into the
|
||||
* listener function passed as argument to `subscribe`. This is done to work around a typing
|
||||
* deficiency in `EventEmitter.subscribe`, where the listener function is typed as any.
|
||||
*/
|
||||
declareOutputHelper(): ts.Expression {
|
||||
if (this.outputHelperIdent !== null) {
|
||||
return this.outputHelperIdent;
|
||||
}
|
||||
|
||||
const outputHelperIdent = ts.createIdentifier('_outputHelper');
|
||||
const genericTypeDecl = ts.createTypeParameterDeclaration('T');
|
||||
const genericTypeRef = ts.createTypeReferenceNode('T', /* typeParameters */ undefined);
|
||||
|
||||
const eventEmitter = this.referenceExternalType(
|
||||
'@angular/core', 'EventEmitter', [new ExpressionType(new WrappedNodeExpr(genericTypeRef))]);
|
||||
|
||||
// Declare a type that has a `subscribe` method that carries over type `T` as parameter
|
||||
// into the callback. The below code generates the following type literal:
|
||||
// `{subscribe(cb: (event: T) => any): void;}`
|
||||
const observableLike = ts.createTypeLiteralNode([ts.createMethodSignature(
|
||||
/* typeParameters */ undefined,
|
||||
/* parameters */[ts.createParameter(
|
||||
/* decorators */ undefined,
|
||||
/* modifiers */ undefined,
|
||||
/* dotDotDotToken */ undefined,
|
||||
/* name */ 'cb',
|
||||
/* questionToken */ undefined,
|
||||
/* type */
|
||||
ts.createFunctionTypeNode(
|
||||
/* typeParameters */ undefined,
|
||||
/* parameters */[ts.createParameter(
|
||||
/* decorators */ undefined,
|
||||
/* modifiers */ undefined,
|
||||
/* dotDotDotToken */ undefined,
|
||||
/* name */ 'event',
|
||||
/* questionToken */ undefined,
|
||||
/* type */ genericTypeRef)],
|
||||
/* type */ ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)))],
|
||||
/* type */ ts.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword),
|
||||
/* name */ 'subscribe',
|
||||
/* questionToken */ undefined)]);
|
||||
|
||||
// Declares the first signature of `_outputHelper` that matches arguments of type
|
||||
// `EventEmitter`, to convert them into `observableLike` defined above. The following
|
||||
// statement is generated:
|
||||
// `declare function _outputHelper<T>(output: EventEmitter<T>): observableLike;`
|
||||
this.helperStatements.push(ts.createFunctionDeclaration(
|
||||
/* decorators */ undefined,
|
||||
/* modifiers */[ts.createModifier(ts.SyntaxKind.DeclareKeyword)],
|
||||
/* asteriskToken */ undefined,
|
||||
/* name */ outputHelperIdent,
|
||||
/* typeParameters */[genericTypeDecl],
|
||||
/* parameters */[ts.createParameter(
|
||||
/* decorators */ undefined,
|
||||
/* modifiers */ undefined,
|
||||
/* dotDotDotToken */ undefined,
|
||||
/* name */ 'output',
|
||||
/* questionToken */ undefined,
|
||||
/* type */ eventEmitter)],
|
||||
/* type */ observableLike,
|
||||
/* body */ undefined));
|
||||
|
||||
// Declares the second signature of `_outputHelper` that matches all other argument types,
|
||||
// i.e. ensures type identity for output types other than `EventEmitter`. This corresponds
|
||||
// with the following statement:
|
||||
// `declare function _outputHelper<T>(output: T): T;`
|
||||
this.helperStatements.push(ts.createFunctionDeclaration(
|
||||
/* decorators */ undefined,
|
||||
/* modifiers */[ts.createModifier(ts.SyntaxKind.DeclareKeyword)],
|
||||
/* asteriskToken */ undefined,
|
||||
/* name */ outputHelperIdent,
|
||||
/* typeParameters */[genericTypeDecl],
|
||||
/* parameters */[ts.createParameter(
|
||||
/* decorators */ undefined,
|
||||
/* modifiers */ undefined,
|
||||
/* dotDotDotToken */ undefined,
|
||||
/* name */ 'output',
|
||||
/* questionToken */ undefined,
|
||||
/* type */ genericTypeRef)],
|
||||
/* type */ genericTypeRef,
|
||||
/* body */ undefined));
|
||||
|
||||
return this.outputHelperIdent = outputHelperIdent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a `ts.Expression` that references the given node.
|
||||
*
|
||||
|
@ -250,7 +160,6 @@ export class Environment {
|
|||
|
||||
getPreludeStatements(): ts.Statement[] {
|
||||
return [
|
||||
...this.helperStatements,
|
||||
...this.pipeInstStatements,
|
||||
...this.typeCtorStatements,
|
||||
];
|
||||
|
|
|
@ -174,7 +174,7 @@ export class SymbolBuilder {
|
|||
}
|
||||
|
||||
// Outputs in the TCB look like one of the two:
|
||||
// * _outputHelper(_t1["outputField"]).subscribe(handler);
|
||||
// * _t1["outputField"].subscribe(handler);
|
||||
// * _t1.addEventListener(handler);
|
||||
// Even with strict null checks disabled, we still produce the access as a separate statement
|
||||
// so that it can be found here.
|
||||
|
|
|
@ -874,17 +874,8 @@ export class TcbDirectiveOutputsOp extends TcbOp {
|
|||
// For strict checking of directive events, generate a call to the `subscribe` method
|
||||
// on the directive's output field to let type information flow into the handler function's
|
||||
// `$event` parameter.
|
||||
//
|
||||
// Note that the `EventEmitter<T>` type from '@angular/core' that is typically used for
|
||||
// outputs has a typings deficiency in its `subscribe` method. The generic type `T` is not
|
||||
// carried into the handler function, which is vital for inference of the type of `$event`.
|
||||
// As a workaround, the directive's field is passed into a helper function that has a
|
||||
// specially crafted set of signatures, to effectively cast `EventEmitter<T>` to something
|
||||
// that has a `subscribe` method that properly carries the `T` into the handler function.
|
||||
const handler = tcbCreateEventHandler(output, this.tcb, this.scope, EventParamType.Infer);
|
||||
const outputHelper =
|
||||
ts.createCall(this.tcb.env.declareOutputHelper(), undefined, [outputField]);
|
||||
const subscribeFn = ts.createPropertyAccess(outputHelper, 'subscribe');
|
||||
const subscribeFn = ts.createPropertyAccess(outputField, 'subscribe');
|
||||
const call = ts.createCall(subscribeFn, /* typeArguments */ undefined, [handler]);
|
||||
addParseSpanInfo(call, output.sourceSpan);
|
||||
this.scope.addStatement(ts.createExpressionStatement(call));
|
||||
|
|
|
@ -56,9 +56,6 @@ export class TypeCheckFile extends Environment {
|
|||
'\n\n';
|
||||
const printer = ts.createPrinter();
|
||||
source += '\n';
|
||||
for (const stmt of this.helperStatements) {
|
||||
source += printer.printNode(ts.EmitHint.Unspecified, stmt, this.contextFile) + '\n';
|
||||
}
|
||||
for (const stmt of this.pipeInstStatements) {
|
||||
source += printer.printNode(ts.EmitHint.Unspecified, stmt, this.contextFile) + '\n';
|
||||
}
|
||||
|
|
|
@ -593,10 +593,6 @@ class FakeEnvironment /* implements Environment */ {
|
|||
return ts.createParen(ts.createAsExpression(ts.createNull(), this.referenceType(ref)));
|
||||
}
|
||||
|
||||
declareOutputHelper(): ts.Expression {
|
||||
return ts.createIdentifier('_outputHelper');
|
||||
}
|
||||
|
||||
reference(ref: Reference<ClassDeclaration<ts.ClassDeclaration>>): ts.Expression {
|
||||
return ref.node.name;
|
||||
}
|
||||
|
|
|
@ -666,7 +666,7 @@ describe('type check blocks', () => {
|
|||
const TEMPLATE = `<div dir (dirOutput)="foo($event)"></div>`;
|
||||
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||
expect(block).toContain(
|
||||
'_outputHelper(_t1["outputField"]).subscribe(function ($event): any { (ctx).foo($event); });');
|
||||
'_t1["outputField"].subscribe(function ($event): any { (ctx).foo($event); });');
|
||||
});
|
||||
|
||||
it('should emit a listener function with AnimationEvent for animation events', () => {
|
||||
|
@ -828,7 +828,7 @@ describe('type check blocks', () => {
|
|||
it('should check types of directive outputs when enabled', () => {
|
||||
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||
expect(block).toContain(
|
||||
'_outputHelper(_t1["outputField"]).subscribe(function ($event): any { (ctx).foo($event); });');
|
||||
'_t1["outputField"].subscribe(function ($event): any { (ctx).foo($event); });');
|
||||
expect(block).toContain(
|
||||
'_t2.addEventListener("nonDirOutput", function ($event): any { (ctx).foo($event); });');
|
||||
});
|
||||
|
@ -865,7 +865,7 @@ describe('type check blocks', () => {
|
|||
it('should check types of DOM events when enabled', () => {
|
||||
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||
expect(block).toContain(
|
||||
'_outputHelper(_t1["outputField"]).subscribe(function ($event): any { (ctx).foo($event); });');
|
||||
'_t1["outputField"].subscribe(function ($event): any { (ctx).foo($event); });');
|
||||
expect(block).toContain(
|
||||
'_t2.addEventListener("nonDirOutput", function ($event): any { (ctx).foo($event); });');
|
||||
});
|
||||
|
@ -875,7 +875,7 @@ describe('type check blocks', () => {
|
|||
// Note that directive outputs are still checked, that is controlled by
|
||||
// `checkTypeOfOutputEvents`
|
||||
expect(block).toContain(
|
||||
'_outputHelper(_t1["outputField"]).subscribe(function ($event): any { (ctx).foo($event); });');
|
||||
'_t1["outputField"].subscribe(function ($event): any { (ctx).foo($event); });');
|
||||
expect(block).toContain('function ($event: any): any { (ctx).foo($event); }');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -48,7 +48,7 @@ export class DefinitionBuilder {
|
|||
const definitions: ts.DefinitionInfo[] = [];
|
||||
for (const definitionMeta of definitionMetas) {
|
||||
// The `$event` of event handlers would point to the $event parameter in the shim file, as in
|
||||
// `_outputHelper(_t3["x"]).subscribe(function ($event): any { $event }) ;`
|
||||
// `_t3["x"].subscribe(function ($event): any { $event }) ;`
|
||||
// If we wanted to return something for this, it would be more appropriate for something like
|
||||
// `getTypeDefinition`.
|
||||
if (isDollarEvent(definitionMeta.node)) {
|
||||
|
|
Loading…
Reference in New Issue