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>();
|
private pipeInsts = new Map<ClassDeclaration, ts.Expression>();
|
||||||
protected pipeInstStatements: ts.Statement[] = [];
|
protected pipeInstStatements: ts.Statement[] = [];
|
||||||
|
|
||||||
private outputHelperIdent: ts.Identifier|null = null;
|
|
||||||
protected helperStatements: ts.Statement[] = [];
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly config: TypeCheckingConfig, protected importManager: ImportManager,
|
readonly config: TypeCheckingConfig, protected importManager: ImportManager,
|
||||||
private refEmitter: ReferenceEmitter, private reflector: ReflectionHost,
|
private refEmitter: ReferenceEmitter, private reflector: ReflectionHost,
|
||||||
|
@ -113,93 +110,6 @@ export class Environment {
|
||||||
return pipeInstId;
|
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.
|
* Generate a `ts.Expression` that references the given node.
|
||||||
*
|
*
|
||||||
|
@ -250,7 +160,6 @@ export class Environment {
|
||||||
|
|
||||||
getPreludeStatements(): ts.Statement[] {
|
getPreludeStatements(): ts.Statement[] {
|
||||||
return [
|
return [
|
||||||
...this.helperStatements,
|
|
||||||
...this.pipeInstStatements,
|
...this.pipeInstStatements,
|
||||||
...this.typeCtorStatements,
|
...this.typeCtorStatements,
|
||||||
];
|
];
|
||||||
|
|
|
@ -174,7 +174,7 @@ export class SymbolBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Outputs in the TCB look like one of the two:
|
// Outputs in the TCB look like one of the two:
|
||||||
// * _outputHelper(_t1["outputField"]).subscribe(handler);
|
// * _t1["outputField"].subscribe(handler);
|
||||||
// * _t1.addEventListener(handler);
|
// * _t1.addEventListener(handler);
|
||||||
// Even with strict null checks disabled, we still produce the access as a separate statement
|
// Even with strict null checks disabled, we still produce the access as a separate statement
|
||||||
// so that it can be found here.
|
// 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
|
// 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
|
// on the directive's output field to let type information flow into the handler function's
|
||||||
// `$event` parameter.
|
// `$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 handler = tcbCreateEventHandler(output, this.tcb, this.scope, EventParamType.Infer);
|
||||||
const outputHelper =
|
const subscribeFn = ts.createPropertyAccess(outputField, 'subscribe');
|
||||||
ts.createCall(this.tcb.env.declareOutputHelper(), undefined, [outputField]);
|
|
||||||
const subscribeFn = ts.createPropertyAccess(outputHelper, 'subscribe');
|
|
||||||
const call = ts.createCall(subscribeFn, /* typeArguments */ undefined, [handler]);
|
const call = ts.createCall(subscribeFn, /* typeArguments */ undefined, [handler]);
|
||||||
addParseSpanInfo(call, output.sourceSpan);
|
addParseSpanInfo(call, output.sourceSpan);
|
||||||
this.scope.addStatement(ts.createExpressionStatement(call));
|
this.scope.addStatement(ts.createExpressionStatement(call));
|
||||||
|
|
|
@ -56,9 +56,6 @@ export class TypeCheckFile extends Environment {
|
||||||
'\n\n';
|
'\n\n';
|
||||||
const printer = ts.createPrinter();
|
const printer = ts.createPrinter();
|
||||||
source += '\n';
|
source += '\n';
|
||||||
for (const stmt of this.helperStatements) {
|
|
||||||
source += printer.printNode(ts.EmitHint.Unspecified, stmt, this.contextFile) + '\n';
|
|
||||||
}
|
|
||||||
for (const stmt of this.pipeInstStatements) {
|
for (const stmt of this.pipeInstStatements) {
|
||||||
source += printer.printNode(ts.EmitHint.Unspecified, stmt, this.contextFile) + '\n';
|
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)));
|
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 {
|
reference(ref: Reference<ClassDeclaration<ts.ClassDeclaration>>): ts.Expression {
|
||||||
return ref.node.name;
|
return ref.node.name;
|
||||||
}
|
}
|
||||||
|
|
|
@ -666,7 +666,7 @@ describe('type check blocks', () => {
|
||||||
const TEMPLATE = `<div dir (dirOutput)="foo($event)"></div>`;
|
const TEMPLATE = `<div dir (dirOutput)="foo($event)"></div>`;
|
||||||
const block = tcb(TEMPLATE, DIRECTIVES);
|
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||||
expect(block).toContain(
|
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', () => {
|
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', () => {
|
it('should check types of directive outputs when enabled', () => {
|
||||||
const block = tcb(TEMPLATE, DIRECTIVES);
|
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||||
expect(block).toContain(
|
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(
|
expect(block).toContain(
|
||||||
'_t2.addEventListener("nonDirOutput", function ($event): any { (ctx).foo($event); });');
|
'_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', () => {
|
it('should check types of DOM events when enabled', () => {
|
||||||
const block = tcb(TEMPLATE, DIRECTIVES);
|
const block = tcb(TEMPLATE, DIRECTIVES);
|
||||||
expect(block).toContain(
|
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(
|
expect(block).toContain(
|
||||||
'_t2.addEventListener("nonDirOutput", function ($event): any { (ctx).foo($event); });');
|
'_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
|
// Note that directive outputs are still checked, that is controlled by
|
||||||
// `checkTypeOfOutputEvents`
|
// `checkTypeOfOutputEvents`
|
||||||
expect(block).toContain(
|
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); }');
|
expect(block).toContain('function ($event: any): any { (ctx).foo($event); }');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -48,7 +48,7 @@ export class DefinitionBuilder {
|
||||||
const definitions: ts.DefinitionInfo[] = [];
|
const definitions: ts.DefinitionInfo[] = [];
|
||||||
for (const definitionMeta of definitionMetas) {
|
for (const definitionMeta of definitionMetas) {
|
||||||
// The `$event` of event handlers would point to the $event parameter in the shim file, as in
|
// 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
|
// If we wanted to return something for this, it would be more appropriate for something like
|
||||||
// `getTypeDefinition`.
|
// `getTypeDefinition`.
|
||||||
if (isDollarEvent(definitionMeta.node)) {
|
if (isDollarEvent(definitionMeta.node)) {
|
||||||
|
|
Loading…
Reference in New Issue