diff --git a/packages/compiler-cli/src/ngtsc/typecheck/BUILD.bazel b/packages/compiler-cli/src/ngtsc/typecheck/BUILD.bazel new file mode 100644 index 0000000000..03cd627259 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/typecheck/BUILD.bazel @@ -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", + ], +) diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/api.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/api.ts new file mode 100644 index 0000000000..4079dfcaa8 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/api.ts @@ -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[];}; +} diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_constructor.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_constructor.ts new file mode 100644 index 0000000000..33019ac51c --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_constructor.ts @@ -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(init: Partial, 'ngForOf'|'ngForTrackBy'|'ngForTemplate'>>): + * NgForOf; + * + * A typical usage would be: + * + * NgForOf.ngTypeCtor(init: {ngForOf: ['foo', 'bar']}); // Infers a type of NgForOf. + * + * @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`, its rawType would be `FooDirective`. + 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 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. + const pickType = ts.createTypeReferenceNode('Pick', [rawType, keyTypeUnion]); + + // Construct the Partial. + 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, ); +}