feat(ivy): introduce typecheck package and a type constructor generator (#26203)
This commit introduces //packages/compiler-cli/src/ngtsc/typecheck as a container for template type-checking code, and implements an initial API: type constructor generation. Type constructors are static methods on component/directive types with no runtime implementation. The methods are used during compilation to enable inference of a component or directive's generic type parameters from the types of expressions bound to any of their @Inputs. A type constructor looks like: class Directive<T> { someInput: T; static ngTypeCtor<T>(init: Partial<Pick<Directive<T>, 'someInput'>>): Directive<T>; } It can be used to infer a type for T based on the input: const _dir = Directive.ngTypeCtor({someInput: 'string'}); // Directive<T> PR Close #26203
This commit is contained in:
parent
9ed4e3df60
commit
b0070dfb9a
|
@ -0,0 +1,15 @@
|
||||||
|
package(default_visibility = ["//visibility:public"])
|
||||||
|
|
||||||
|
load("//tools:defaults.bzl", "ts_library")
|
||||||
|
|
||||||
|
ts_library(
|
||||||
|
name = "typecheck",
|
||||||
|
srcs = glob(["**/*.ts"]),
|
||||||
|
module_name = "@angular/compiler-cli/src/ngtsc/typecheck",
|
||||||
|
deps = [
|
||||||
|
"//packages:types",
|
||||||
|
"//packages/compiler",
|
||||||
|
"//packages/compiler-cli/src/ngtsc/metadata",
|
||||||
|
"//packages/compiler-cli/src/ngtsc/util",
|
||||||
|
],
|
||||||
|
)
|
|
@ -0,0 +1,27 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata to generate a type constructor for a particular directive.
|
||||||
|
*/
|
||||||
|
export interface TypeCtorMetadata {
|
||||||
|
/**
|
||||||
|
* The name of the requested type constructor function.
|
||||||
|
*/
|
||||||
|
fnName: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to generate a body for the function or not.
|
||||||
|
*/
|
||||||
|
body: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Input, output, and query field names in the type which should be included as constructor input.
|
||||||
|
*/
|
||||||
|
fields: {inputs: string[]; outputs: string[]; queries: string[];};
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
import {TypeCtorMetadata} from './api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a type constructor for the given class and metadata.
|
||||||
|
*
|
||||||
|
* A type constructor is a specially shaped TypeScript static method, intended to be placed within
|
||||||
|
* a directive class itself, that permits type inference of any generic type parameters of the class
|
||||||
|
* from the types of expressions bound to inputs or outputs, and the types of elements that match
|
||||||
|
* queries performed by the directive. It also catches any errors in the types of these expressions.
|
||||||
|
* This method is never called at runtime, but is used in type-check blocks to construct directive
|
||||||
|
* types.
|
||||||
|
*
|
||||||
|
* A type constructor for NgFor looks like:
|
||||||
|
*
|
||||||
|
* static ngTypeCtor<T>(init: Partial<Pick<NgForOf<T>, 'ngForOf'|'ngForTrackBy'|'ngForTemplate'>>):
|
||||||
|
* NgForOf<T>;
|
||||||
|
*
|
||||||
|
* A typical usage would be:
|
||||||
|
*
|
||||||
|
* NgForOf.ngTypeCtor(init: {ngForOf: ['foo', 'bar']}); // Infers a type of NgForOf<string>.
|
||||||
|
*
|
||||||
|
* @param node the `ts.ClassDeclaration` for which a type constructor will be generated.
|
||||||
|
* @param meta additional metadata required to generate the type constructor.
|
||||||
|
* @returns a `ts.MethodDeclaration` for the type constructor.
|
||||||
|
*/
|
||||||
|
export function generateTypeCtor(
|
||||||
|
node: ts.ClassDeclaration, meta: TypeCtorMetadata): ts.MethodDeclaration {
|
||||||
|
// Build rawType, a `ts.TypeNode` of the class with its generic parameters passed through from
|
||||||
|
// the definition without any type bounds. For example, if the class is
|
||||||
|
// `FooDirective<T extends Bar>`, its rawType would be `FooDirective<T>`.
|
||||||
|
const rawTypeArgs = node.typeParameters !== undefined ?
|
||||||
|
node.typeParameters.map(param => ts.createTypeReferenceNode(param.name, undefined)) :
|
||||||
|
undefined;
|
||||||
|
const rawType: ts.TypeNode = ts.createTypeReferenceNode(node.name !, rawTypeArgs);
|
||||||
|
|
||||||
|
// initType is the type of 'init', the single argument to the type constructor method.
|
||||||
|
// If the Directive has any inputs, outputs, or queries, its initType will be:
|
||||||
|
//
|
||||||
|
// Partial<Pick<rawType, 'inputField'|'outputField'|'queryField'>>
|
||||||
|
//
|
||||||
|
// Pick here is used to select only those fields from which the generic type parameters of the
|
||||||
|
// directive will be inferred. Partial is used because inputs are optional, so there may not be
|
||||||
|
// bindings for each field.
|
||||||
|
//
|
||||||
|
// In the special case there are no inputs/outputs/etc, initType is set to {}.
|
||||||
|
let initType: ts.TypeNode;
|
||||||
|
|
||||||
|
const keys: string[] = [
|
||||||
|
...meta.fields.inputs,
|
||||||
|
...meta.fields.outputs,
|
||||||
|
...meta.fields.queries,
|
||||||
|
];
|
||||||
|
if (keys.length === 0) {
|
||||||
|
// Special case - no inputs, outputs, or other fields which could influence the result type.
|
||||||
|
initType = ts.createTypeLiteralNode([]);
|
||||||
|
} else {
|
||||||
|
// Construct a union of all the field names.
|
||||||
|
const keyTypeUnion = ts.createUnionTypeNode(
|
||||||
|
keys.map(key => ts.createLiteralTypeNode(ts.createStringLiteral(key))));
|
||||||
|
|
||||||
|
// Construct the Pick<rawType, keyTypeUnion>.
|
||||||
|
const pickType = ts.createTypeReferenceNode('Pick', [rawType, keyTypeUnion]);
|
||||||
|
|
||||||
|
// Construct the Partial<pickType>.
|
||||||
|
initType = ts.createTypeReferenceNode('Partial', [pickType]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this constructor is being generated into a .ts file, then it needs a fake body. The body
|
||||||
|
// is set to a return of `null!`. If the type constructor is being generated into a .d.ts file,
|
||||||
|
// it needs no body.
|
||||||
|
let body: ts.Block|undefined = undefined;
|
||||||
|
if (meta.body) {
|
||||||
|
body = ts.createBlock([
|
||||||
|
ts.createReturn(ts.createNonNullExpression(ts.createNull())),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the 'init' parameter itself.
|
||||||
|
const initParam = ts.createParameter(
|
||||||
|
/* decorators */ undefined,
|
||||||
|
/* modifiers */ undefined,
|
||||||
|
/* dotDotDotToken */ undefined,
|
||||||
|
/* name */ 'init',
|
||||||
|
/* questionToken */ undefined,
|
||||||
|
/* type */ initType,
|
||||||
|
/* initializer */ undefined, );
|
||||||
|
|
||||||
|
// Create the type constructor method declaration.
|
||||||
|
return ts.createMethod(
|
||||||
|
/* decorators */ undefined,
|
||||||
|
/* modifiers */[ts.createModifier(ts.SyntaxKind.StaticKeyword)],
|
||||||
|
/* asteriskToken */ undefined,
|
||||||
|
/* name */ meta.fnName,
|
||||||
|
/* questionToken */ undefined,
|
||||||
|
/* typeParameters */ node.typeParameters,
|
||||||
|
/* parameters */[initParam],
|
||||||
|
/* type */ rawType,
|
||||||
|
/* body */ body, );
|
||||||
|
}
|
Loading…
Reference in New Issue