Skip to content

Commit b0070df

Browse files
alxhubjasonaden
authored andcommitted
feat(ivy): introduce typecheck package and a type constructor generator (angular#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 angular#26203
1 parent 9ed4e3d commit b0070df

File tree

3 files changed

+151
-0
lines changed

3 files changed

+151
-0
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package(default_visibility = ["//visibility:public"])
2+
3+
load("//tools:defaults.bzl", "ts_library")
4+
5+
ts_library(
6+
name = "typecheck",
7+
srcs = glob(["**/*.ts"]),
8+
module_name = "@angular/compiler-cli/src/ngtsc/typecheck",
9+
deps = [
10+
"//packages:types",
11+
"//packages/compiler",
12+
"//packages/compiler-cli/src/ngtsc/metadata",
13+
"//packages/compiler-cli/src/ngtsc/util",
14+
],
15+
)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
/**
10+
* Metadata to generate a type constructor for a particular directive.
11+
*/
12+
export interface TypeCtorMetadata {
13+
/**
14+
* The name of the requested type constructor function.
15+
*/
16+
fnName: string;
17+
18+
/**
19+
* Whether to generate a body for the function or not.
20+
*/
21+
body: boolean;
22+
23+
/**
24+
* Input, output, and query field names in the type which should be included as constructor input.
25+
*/
26+
fields: {inputs: string[]; outputs: string[]; queries: string[];};
27+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import * as ts from 'typescript';
10+
11+
import {TypeCtorMetadata} from './api';
12+
13+
/**
14+
* Generate a type constructor for the given class and metadata.
15+
*
16+
* A type constructor is a specially shaped TypeScript static method, intended to be placed within
17+
* a directive class itself, that permits type inference of any generic type parameters of the class
18+
* from the types of expressions bound to inputs or outputs, and the types of elements that match
19+
* queries performed by the directive. It also catches any errors in the types of these expressions.
20+
* This method is never called at runtime, but is used in type-check blocks to construct directive
21+
* types.
22+
*
23+
* A type constructor for NgFor looks like:
24+
*
25+
* static ngTypeCtor<T>(init: Partial<Pick<NgForOf<T>, 'ngForOf'|'ngForTrackBy'|'ngForTemplate'>>):
26+
* NgForOf<T>;
27+
*
28+
* A typical usage would be:
29+
*
30+
* NgForOf.ngTypeCtor(init: {ngForOf: ['foo', 'bar']}); // Infers a type of NgForOf<string>.
31+
*
32+
* @param node the `ts.ClassDeclaration` for which a type constructor will be generated.
33+
* @param meta additional metadata required to generate the type constructor.
34+
* @returns a `ts.MethodDeclaration` for the type constructor.
35+
*/
36+
export function generateTypeCtor(
37+
node: ts.ClassDeclaration, meta: TypeCtorMetadata): ts.MethodDeclaration {
38+
// Build rawType, a `ts.TypeNode` of the class with its generic parameters passed through from
39+
// the definition without any type bounds. For example, if the class is
40+
// `FooDirective<T extends Bar>`, its rawType would be `FooDirective<T>`.
41+
const rawTypeArgs = node.typeParameters !== undefined ?
42+
node.typeParameters.map(param => ts.createTypeReferenceNode(param.name, undefined)) :
43+
undefined;
44+
const rawType: ts.TypeNode = ts.createTypeReferenceNode(node.name !, rawTypeArgs);
45+
46+
// initType is the type of 'init', the single argument to the type constructor method.
47+
// If the Directive has any inputs, outputs, or queries, its initType will be:
48+
//
49+
// Partial<Pick<rawType, 'inputField'|'outputField'|'queryField'>>
50+
//
51+
// Pick here is used to select only those fields from which the generic type parameters of the
52+
// directive will be inferred. Partial is used because inputs are optional, so there may not be
53+
// bindings for each field.
54+
//
55+
// In the special case there are no inputs/outputs/etc, initType is set to {}.
56+
let initType: ts.TypeNode;
57+
58+
const keys: string[] = [
59+
...meta.fields.inputs,
60+
...meta.fields.outputs,
61+
...meta.fields.queries,
62+
];
63+
if (keys.length === 0) {
64+
// Special case - no inputs, outputs, or other fields which could influence the result type.
65+
initType = ts.createTypeLiteralNode([]);
66+
} else {
67+
// Construct a union of all the field names.
68+
const keyTypeUnion = ts.createUnionTypeNode(
69+
keys.map(key => ts.createLiteralTypeNode(ts.createStringLiteral(key))));
70+
71+
// Construct the Pick<rawType, keyTypeUnion>.
72+
const pickType = ts.createTypeReferenceNode('Pick', [rawType, keyTypeUnion]);
73+
74+
// Construct the Partial<pickType>.
75+
initType = ts.createTypeReferenceNode('Partial', [pickType]);
76+
}
77+
78+
// If this constructor is being generated into a .ts file, then it needs a fake body. The body
79+
// is set to a return of `null!`. If the type constructor is being generated into a .d.ts file,
80+
// it needs no body.
81+
let body: ts.Block|undefined = undefined;
82+
if (meta.body) {
83+
body = ts.createBlock([
84+
ts.createReturn(ts.createNonNullExpression(ts.createNull())),
85+
]);
86+
}
87+
88+
// Create the 'init' parameter itself.
89+
const initParam = ts.createParameter(
90+
/* decorators */ undefined,
91+
/* modifiers */ undefined,
92+
/* dotDotDotToken */ undefined,
93+
/* name */ 'init',
94+
/* questionToken */ undefined,
95+
/* type */ initType,
96+
/* initializer */ undefined, );
97+
98+
// Create the type constructor method declaration.
99+
return ts.createMethod(
100+
/* decorators */ undefined,
101+
/* modifiers */[ts.createModifier(ts.SyntaxKind.StaticKeyword)],
102+
/* asteriskToken */ undefined,
103+
/* name */ meta.fnName,
104+
/* questionToken */ undefined,
105+
/* typeParameters */ node.typeParameters,
106+
/* parameters */[initParam],
107+
/* type */ rawType,
108+
/* body */ body, );
109+
}

0 commit comments

Comments
 (0)