Skip to content

Account for type-only import specifier in TS 4.5 #43620

@JoostK

Description

@JoostK

For upcoming TS 4.5 our typeToValue logic needs to account for microsoft/TypeScript#45998, where we should report an error if an import specifier is used that has a type-only modifier for a DI token.

export function typeToValue(
typeNode: ts.TypeNode|null, checker: ts.TypeChecker): TypeValueReference {
// It's not possible to get a value expression if the parameter doesn't even have a type.
if (typeNode === null) {
return missingType();
}
if (!ts.isTypeReferenceNode(typeNode)) {
return unsupportedType(typeNode);
}
const symbols = resolveTypeSymbols(typeNode, checker);
if (symbols === null) {
return unknownReference(typeNode);
}
const {local, decl} = symbols;
// It's only valid to convert a type reference to a value reference if the type actually
// has a value declaration associated with it. Note that const enums are an exception,
// because while they do have a value declaration, they don't exist at runtime.
if (decl.valueDeclaration === undefined || decl.flags & ts.SymbolFlags.ConstEnum) {
let typeOnlyDecl: ts.Declaration|null = null;
if (decl.declarations !== undefined && decl.declarations.length > 0) {
typeOnlyDecl = decl.declarations[0];
}
return noValueDeclaration(typeNode, typeOnlyDecl);
}
// The type points to a valid value declaration. Rewrite the TypeReference into an
// Expression which references the value pointed to by the TypeReference, if possible.
// Look at the local `ts.Symbol`'s declarations and see if it comes from an import
// statement. If so, extract the module specifier and the name of the imported type.
const firstDecl = local.declarations && local.declarations[0];
if (firstDecl !== undefined) {
if (ts.isImportClause(firstDecl) && firstDecl.name !== undefined) {
// This is a default import.
// import Foo from 'foo';
if (firstDecl.isTypeOnly) {
// Type-only imports cannot be represented as value.
return typeOnlyImport(typeNode, firstDecl);
}
return {
kind: TypeValueReferenceKind.LOCAL,
expression: firstDecl.name,
defaultImportStatement: firstDecl.parent,
};
} else if (ts.isImportSpecifier(firstDecl)) {
// The symbol was imported by name
// import {Foo} from 'foo';
// or
// import {Foo as Bar} from 'foo';
if (firstDecl.parent.parent.isTypeOnly) {
// Type-only imports cannot be represented as value.
return typeOnlyImport(typeNode, firstDecl.parent.parent);
}
// Determine the name to import (`Foo`) from the import specifier, as the symbol names of
// the imported type could refer to a local alias (like `Bar` in the example above).
const importedName = (firstDecl.propertyName || firstDecl.name).text;
// The first symbol name refers to the local name, which is replaced by `importedName` above.
// Any remaining symbol names make up the complete path to the value.
const [_localName, ...nestedPath] = symbols.symbolNames;
const moduleName = extractModuleName(firstDecl.parent.parent.parent);
return {
kind: TypeValueReferenceKind.IMPORTED,
valueDeclaration: decl.valueDeclaration,
moduleName,
importedName,
nestedPath
};
} else if (ts.isNamespaceImport(firstDecl)) {
// The import is a namespace import
// import * as Foo from 'foo';
if (firstDecl.parent.isTypeOnly) {
// Type-only imports cannot be represented as value.
return typeOnlyImport(typeNode, firstDecl.parent);
}
if (symbols.symbolNames.length === 1) {
// The type refers to the namespace itself, which cannot be represented as a value.
return namespaceImport(typeNode, firstDecl.parent);
}
// The first symbol name refers to the local name of the namespace, which is is discarded
// as a new namespace import will be generated. This is followed by the symbol name that needs
// to be imported and any remaining names that constitute the complete path to the value.
const [_ns, importedName, ...nestedPath] = symbols.symbolNames;
const moduleName = extractModuleName(firstDecl.parent.parent);
return {
kind: TypeValueReferenceKind.IMPORTED,
valueDeclaration: decl.valueDeclaration,
moduleName,
importedName,
nestedPath
};
}
}
// If the type is not imported, the type reference can be converted into an expression as is.
const expression = typeNodeToValueExpr(typeNode);
if (expression !== null) {
return {
kind: TypeValueReferenceKind.LOCAL,
expression,
defaultImportStatement: null,
};
} else {
return unsupportedType(typeNode);
}
}

For example the following should be invalid:

import { type MyService } from './service.ts';

@Injectable()
export class OtherService {
  constructor(private service: MyService) {}
}

The compiler is already capable of reporting errors for type-only import statements, it just needs to be expanded for type-only import specifiers.

Metadata

Metadata

Assignees

Labels

area: compilerIssues related to `ngc`, Angular's template compilerrefactoringIssue that involves refactoring or code-cleanupstate: has PR

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions