Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support 'typeof class' types #41587

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12521,7 +12521,9 @@ namespace ts {
// The expression is processed as an identifier expression (section 4.3)
// or property access expression(section 4.10),
// the widened type(section 3.9) of which becomes the result.
links.resolvedType = getRegularTypeOfLiteralType(getWidenedType(checkExpression(node.exprName)));
links.resolvedType = node.exprName.kind === SyntaxKind.ClassExpression ?
checkClassExpression(node.exprName) :
getRegularTypeOfLiteralType(getWidenedType(checkExpression(node.exprName)));
}
return links.resolvedType;
}
Expand Down Expand Up @@ -38978,7 +38980,8 @@ namespace ts {
if (flags & ModifierFlags.Abstract) {
return grammarErrorOnNode(modifier, Diagnostics._0_modifier_already_seen, "abstract");
}
if (node.kind !== SyntaxKind.ClassDeclaration) {
// An abstract modifier is permitted on a class expression in a 'typeof abstract class {}' type
if (node.kind !== SyntaxKind.ClassDeclaration && node.kind !== SyntaxKind.ClassExpression) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't that also make it valid for a regular class expression? If not, could we consider allowing it in parse? It's never made sense to me that you can write this:

function f() {
  abstract class C {}
  return C;
}

But not this:

function f() {
  return abstract class C {};
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We certainly could allow it, but it would in effect be an expression syntax extension and we generally try to avoid those.

if (node.kind !== SyntaxKind.MethodDeclaration &&
node.kind !== SyntaxKind.PropertyDeclaration &&
node.kind !== SyntaxKind.GetAccessor &&
Expand Down Expand Up @@ -39092,6 +39095,8 @@ namespace ts {
case SyntaxKind.FunctionDeclaration:
return nodeHasAnyModifiersExcept(node, SyntaxKind.AsyncKeyword);
case SyntaxKind.ClassDeclaration:
case SyntaxKind.ClassExpression:
// An abstract modifier is permitted on a class expression in a 'typeof abstract class {}' type
return nodeHasAnyModifiersExcept(node, SyntaxKind.AbstractKeyword);
case SyntaxKind.InterfaceDeclaration:
case SyntaxKind.VariableStatement:
Expand Down
4 changes: 2 additions & 2 deletions src/compiler/factory/nodeFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1733,15 +1733,15 @@ namespace ts {
}

// @api
function createTypeQueryNode(exprName: EntityName) {
function createTypeQueryNode(exprName: EntityName | ClassExpression) {
const node = createBaseNode<TypeQueryNode>(SyntaxKind.TypeQuery);
node.exprName = exprName;
node.transformFlags = TransformFlags.ContainsTypeScript;
return node;
}

// @api
function updateTypeQueryNode(node: TypeQueryNode, exprName: EntityName) {
function updateTypeQueryNode(node: TypeQueryNode, exprName: EntityName | ClassExpression) {
return node.exprName !== exprName
? update(createTypeQueryNode(exprName), node)
: node;
Expand Down
16 changes: 15 additions & 1 deletion src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2860,10 +2860,24 @@ namespace ts {
return type;
}

function isStartOfTypeofClassExpression() {
return token() === SyntaxKind.ClassKeyword ||
token() === SyntaxKind.AbstractKeyword && lookAhead(() => nextToken() === SyntaxKind.ClassKeyword && !scanner.hasPrecedingLineBreak());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I mention in my comment in checker.ts, is there a reason we don't permit abstract for class expressions? It seems like we should always do this, not just for typeof.

}

function parseTypeofClassExpression(): ClassExpression {
const pos = getNodePos();
const hasJSDoc = hasPrecedingJSDocComment();
const modifiers = parseModifiers();
return <ClassExpression>parseClassDeclarationOrExpression(pos, hasJSDoc, /*decorators*/ undefined, modifiers, SyntaxKind.ClassExpression);
}

function parseTypeQuery(): TypeQueryNode {
const pos = getNodePos();
parseExpected(SyntaxKind.TypeOfKeyword);
return finishNode(factory.createTypeQueryNode(parseEntityName(/*allowReservedWords*/ true)), pos);
return finishNode(factory.createTypeQueryNode(isStartOfTypeofClassExpression() ?
doInsideOfContext(NodeFlags.Ambient, parseTypeofClassExpression) :
parseEntityName(/*allowReservedWords*/ true)), pos);
}

function parseTypeParameter(): TypeParameterDeclaration {
Expand Down
6 changes: 4 additions & 2 deletions src/compiler/symbolWalker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,10 @@ namespace ts {
// query node on any of the symbol's declarations and get symbols there
if ((d as any).type && (d as any).type.kind === SyntaxKind.TypeQuery) {
const query = (d as any).type as TypeQueryNode;
const entity = getResolvedSymbol(getFirstIdentifier(query.exprName));
visitSymbol(entity);
if (query.exprName.kind !== SyntaxKind.ClassExpression) {
const entity = getResolvedSymbol(getFirstIdentifier(query.exprName));
visitSymbol(entity);
}
}
});
return false;
Expand Down
3 changes: 2 additions & 1 deletion src/compiler/transformers/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,7 @@ namespace ts {
|| isTypeAliasDeclaration(node)
|| isModuleDeclaration(node)
|| isClassDeclaration(node)
|| isClassExpression(node)
|| isInterfaceDeclaration(node)
|| isFunctionLike(node)
|| isIndexSignatureDeclaration(node)
Expand Down Expand Up @@ -848,7 +849,7 @@ namespace ts {
getSymbolAccessibilityDiagnostic = createGetSymbolAccessibilityDiagnosticForNode(input as DeclarationDiagnosticProducing);
}

if (isTypeQueryNode(input)) {
if (isTypeQueryNode(input) && input.exprName.kind !== SyntaxKind.ClassExpression) {
checkEntityNameVisibility(input.exprName, enclosingDeclaration);
}

Expand Down
6 changes: 3 additions & 3 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1543,7 +1543,7 @@ namespace ts {

export interface TypeQueryNode extends TypeNode {
readonly kind: SyntaxKind.TypeQuery;
readonly exprName: EntityName;
readonly exprName: EntityName | ClassExpression;
}

// A TypeLiteral is the declaration node for an anonymous symbol.
Expand Down Expand Up @@ -6801,8 +6801,8 @@ namespace ts {
updateFunctionTypeNode(node: FunctionTypeNode, typeParameters: NodeArray<TypeParameterDeclaration> | undefined, parameters: NodeArray<ParameterDeclaration>, type: TypeNode): FunctionTypeNode;
createConstructorTypeNode(typeParameters: readonly TypeParameterDeclaration[] | undefined, parameters: readonly ParameterDeclaration[], type: TypeNode): ConstructorTypeNode;
updateConstructorTypeNode(node: ConstructorTypeNode, typeParameters: NodeArray<TypeParameterDeclaration> | undefined, parameters: NodeArray<ParameterDeclaration>, type: TypeNode): ConstructorTypeNode;
createTypeQueryNode(exprName: EntityName): TypeQueryNode;
updateTypeQueryNode(node: TypeQueryNode, exprName: EntityName): TypeQueryNode;
createTypeQueryNode(exprName: EntityName | ClassExpression): TypeQueryNode;
updateTypeQueryNode(node: TypeQueryNode, exprName: EntityName | ClassExpression): TypeQueryNode;
createTypeLiteralNode(members: readonly TypeElement[] | undefined): TypeLiteralNode;
updateTypeLiteralNode(node: TypeLiteralNode, members: NodeArray<TypeElement>): TypeLiteralNode;
createArrayTypeNode(elementType: TypeNode): ArrayTypeNode;
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/utilitiesPublic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1176,6 +1176,10 @@ namespace ts {
|| kind === SyntaxKind.Identifier;
}

export function isEntityNameOrClassExpression(node: Node): node is EntityName {
return isEntityName(node) || node.kind === SyntaxKind.ClassExpression;
}

export function isPropertyName(node: Node): node is PropertyName {
const kind = node.kind;
return kind === SyntaxKind.Identifier
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/visitorPublic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ namespace ts {

case SyntaxKind.TypeQuery:
return factory.updateTypeQueryNode((<TypeQueryNode>node),
nodeVisitor((<TypeQueryNode>node).exprName, visitor, isEntityName));
nodeVisitor((<TypeQueryNode>node).exprName, visitor, isEntityNameOrClassExpression));

case SyntaxKind.TypeLiteral:
return factory.updateTypeLiteralNode((<TypeLiteralNode>node),
Expand Down
2 changes: 1 addition & 1 deletion src/services/refactors/extractType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ namespace ts.refactor {
return true;
}
}
else if (isTypeQueryNode(node)) {
else if (isTypeQueryNode(node) && node.exprName.kind !== SyntaxKind.ClassExpression) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wondering out loud: it might be useful to make an isTypeQueryableNode helper or similar, since this is already used twice internally, and it'd be helpful for consumers?

if (isIdentifier(node.exprName)) {
const symbol = checker.resolveName(node.exprName.text, node.exprName, SymbolFlags.Value, /* excludeGlobals */ false);
if (symbol && rangeContainsSkipTrivia(statement, symbol.valueDeclaration, file) && !rangeContainsSkipTrivia(selection, symbol.valueDeclaration, file)) {
Expand Down
11 changes: 6 additions & 5 deletions tests/baselines/reference/api/tsserverlibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -881,7 +881,7 @@ declare namespace ts {
}
export interface TypeQueryNode extends TypeNode {
readonly kind: SyntaxKind.TypeQuery;
readonly exprName: EntityName;
readonly exprName: EntityName | ClassExpression;
}
export interface TypeLiteralNode extends TypeNode, Declaration {
readonly kind: SyntaxKind.TypeLiteral;
Expand Down Expand Up @@ -3234,8 +3234,8 @@ declare namespace ts {
updateFunctionTypeNode(node: FunctionTypeNode, typeParameters: NodeArray<TypeParameterDeclaration> | undefined, parameters: NodeArray<ParameterDeclaration>, type: TypeNode): FunctionTypeNode;
createConstructorTypeNode(typeParameters: readonly TypeParameterDeclaration[] | undefined, parameters: readonly ParameterDeclaration[], type: TypeNode): ConstructorTypeNode;
updateConstructorTypeNode(node: ConstructorTypeNode, typeParameters: NodeArray<TypeParameterDeclaration> | undefined, parameters: NodeArray<ParameterDeclaration>, type: TypeNode): ConstructorTypeNode;
createTypeQueryNode(exprName: EntityName): TypeQueryNode;
updateTypeQueryNode(node: TypeQueryNode, exprName: EntityName): TypeQueryNode;
createTypeQueryNode(exprName: EntityName | ClassExpression): TypeQueryNode;
updateTypeQueryNode(node: TypeQueryNode, exprName: EntityName | ClassExpression): TypeQueryNode;
createTypeLiteralNode(members: readonly TypeElement[] | undefined): TypeLiteralNode;
updateTypeLiteralNode(node: TypeLiteralNode, members: NodeArray<TypeElement>): TypeLiteralNode;
createArrayTypeNode(elementType: TypeNode): ArrayTypeNode;
Expand Down Expand Up @@ -4214,6 +4214,7 @@ declare namespace ts {
function isStringTextContainingNode(node: Node): node is StringLiteral | TemplateLiteralToken;
function isModifier(node: Node): node is Modifier;
function isEntityName(node: Node): node is EntityName;
function isEntityNameOrClassExpression(node: Node): node is EntityName;
function isPropertyName(node: Node): node is PropertyName;
function isBindingName(node: Node): node is BindingName;
function isFunctionLike(node: Node): node is SignatureDeclaration;
Expand Down Expand Up @@ -10183,9 +10184,9 @@ declare namespace ts {
/** @deprecated Use `factory.updateConstructorTypeNode` or the factory supplied by your transformation context instead. */
const updateConstructorTypeNode: (node: ConstructorTypeNode, typeParameters: NodeArray<TypeParameterDeclaration> | undefined, parameters: NodeArray<ParameterDeclaration>, type: TypeNode) => ConstructorTypeNode;
/** @deprecated Use `factory.createTypeQueryNode` or the factory supplied by your transformation context instead. */
const createTypeQueryNode: (exprName: EntityName) => TypeQueryNode;
const createTypeQueryNode: (exprName: Identifier | ClassExpression | QualifiedName) => TypeQueryNode;
/** @deprecated Use `factory.updateTypeQueryNode` or the factory supplied by your transformation context instead. */
const updateTypeQueryNode: (node: TypeQueryNode, exprName: EntityName) => TypeQueryNode;
const updateTypeQueryNode: (node: TypeQueryNode, exprName: Identifier | ClassExpression | QualifiedName) => TypeQueryNode;
/** @deprecated Use `factory.createTypeLiteralNode` or the factory supplied by your transformation context instead. */
const createTypeLiteralNode: (members: readonly TypeElement[] | undefined) => TypeLiteralNode;
/** @deprecated Use `factory.updateTypeLiteralNode` or the factory supplied by your transformation context instead. */
Expand Down
11 changes: 6 additions & 5 deletions tests/baselines/reference/api/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -881,7 +881,7 @@ declare namespace ts {
}
export interface TypeQueryNode extends TypeNode {
readonly kind: SyntaxKind.TypeQuery;
readonly exprName: EntityName;
readonly exprName: EntityName | ClassExpression;
}
export interface TypeLiteralNode extends TypeNode, Declaration {
readonly kind: SyntaxKind.TypeLiteral;
Expand Down Expand Up @@ -3234,8 +3234,8 @@ declare namespace ts {
updateFunctionTypeNode(node: FunctionTypeNode, typeParameters: NodeArray<TypeParameterDeclaration> | undefined, parameters: NodeArray<ParameterDeclaration>, type: TypeNode): FunctionTypeNode;
createConstructorTypeNode(typeParameters: readonly TypeParameterDeclaration[] | undefined, parameters: readonly ParameterDeclaration[], type: TypeNode): ConstructorTypeNode;
updateConstructorTypeNode(node: ConstructorTypeNode, typeParameters: NodeArray<TypeParameterDeclaration> | undefined, parameters: NodeArray<ParameterDeclaration>, type: TypeNode): ConstructorTypeNode;
createTypeQueryNode(exprName: EntityName): TypeQueryNode;
updateTypeQueryNode(node: TypeQueryNode, exprName: EntityName): TypeQueryNode;
createTypeQueryNode(exprName: EntityName | ClassExpression): TypeQueryNode;
updateTypeQueryNode(node: TypeQueryNode, exprName: EntityName | ClassExpression): TypeQueryNode;
createTypeLiteralNode(members: readonly TypeElement[] | undefined): TypeLiteralNode;
updateTypeLiteralNode(node: TypeLiteralNode, members: NodeArray<TypeElement>): TypeLiteralNode;
createArrayTypeNode(elementType: TypeNode): ArrayTypeNode;
Expand Down Expand Up @@ -4214,6 +4214,7 @@ declare namespace ts {
function isStringTextContainingNode(node: Node): node is StringLiteral | TemplateLiteralToken;
function isModifier(node: Node): node is Modifier;
function isEntityName(node: Node): node is EntityName;
function isEntityNameOrClassExpression(node: Node): node is EntityName;
function isPropertyName(node: Node): node is PropertyName;
function isBindingName(node: Node): node is BindingName;
function isFunctionLike(node: Node): node is SignatureDeclaration;
Expand Down Expand Up @@ -6543,9 +6544,9 @@ declare namespace ts {
/** @deprecated Use `factory.updateConstructorTypeNode` or the factory supplied by your transformation context instead. */
const updateConstructorTypeNode: (node: ConstructorTypeNode, typeParameters: NodeArray<TypeParameterDeclaration> | undefined, parameters: NodeArray<ParameterDeclaration>, type: TypeNode) => ConstructorTypeNode;
/** @deprecated Use `factory.createTypeQueryNode` or the factory supplied by your transformation context instead. */
const createTypeQueryNode: (exprName: EntityName) => TypeQueryNode;
const createTypeQueryNode: (exprName: Identifier | ClassExpression | QualifiedName) => TypeQueryNode;
/** @deprecated Use `factory.updateTypeQueryNode` or the factory supplied by your transformation context instead. */
const updateTypeQueryNode: (node: TypeQueryNode, exprName: EntityName) => TypeQueryNode;
const updateTypeQueryNode: (node: TypeQueryNode, exprName: Identifier | ClassExpression | QualifiedName) => TypeQueryNode;
/** @deprecated Use `factory.createTypeLiteralNode` or the factory supplied by your transformation context instead. */
const createTypeLiteralNode: (members: readonly TypeElement[] | undefined) => TypeLiteralNode;
/** @deprecated Use `factory.updateTypeLiteralNode` or the factory supplied by your transformation context instead. */
Expand Down
7 changes: 7 additions & 0 deletions tests/baselines/reference/jsdocTypeTag.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ var f;

/** @type {new (s: string) => { s: string }} */
var ctor;

/** @type {typeof class { s: string; static n: number }} */
var ctor2;

//// [b.ts]
var S: string;
Expand All @@ -90,6 +93,7 @@ var obj: any;
var Func: Function;
var f: (s: string) => number;
var ctor: new (s: string) => { s: string };
var ctor2: typeof class { s: string; static n: number };


//// [a.js]
Expand Down Expand Up @@ -137,6 +141,8 @@ var Func;
var f;
/** @type {new (s: string) => { s: string }} */
var ctor;
/** @type {typeof class { s: string; static n: number }} */
var ctor2;
//// [b.js]
var S;
var s;
Expand All @@ -160,3 +166,4 @@ var obj;
var Func;
var f;
var ctor;
var ctor2;
9 changes: 9 additions & 0 deletions tests/baselines/reference/jsdocTypeTag.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ var f;
var ctor;
>ctor : Symbol(ctor, Decl(a.js, 64, 3), Decl(b.ts, 21, 3))

/** @type {typeof class { s: string; static n: number }} */
var ctor2;
>ctor2 : Symbol(ctor2, Decl(a.js, 67, 3), Decl(b.ts, 22, 3))

=== tests/cases/conformance/jsdoc/b.ts ===
var S: string;
>S : Symbol(S, Decl(a.js, 1, 3), Decl(b.ts, 0, 3))
Expand Down Expand Up @@ -160,3 +164,8 @@ var ctor: new (s: string) => { s: string };
>s : Symbol(s, Decl(b.ts, 21, 15))
>s : Symbol(s, Decl(b.ts, 21, 30))

var ctor2: typeof class { s: string; static n: number };
>ctor2 : Symbol(ctor2, Decl(a.js, 67, 3), Decl(b.ts, 22, 3))
>s : Symbol((Anonymous class).s, Decl(b.ts, 22, 25))
>n : Symbol((Anonymous class).n, Decl(b.ts, 22, 36))

10 changes: 10 additions & 0 deletions tests/baselines/reference/jsdocTypeTag.types
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ var f;
var ctor;
>ctor : new (s: string) => { s: string; }

/** @type {typeof class { s: string; static n: number }} */
var ctor2;
>ctor2 : typeof (Anonymous class)

=== tests/cases/conformance/jsdoc/b.ts ===
var S: string;
>S : string
Expand Down Expand Up @@ -160,3 +164,9 @@ var ctor: new (s: string) => { s: string };
>s : string
>s : string

var ctor2: typeof class { s: string; static n: number };
>ctor2 : typeof (Anonymous class)
>class { s: string; static n: number } : typeof (Anonymous class)
>s : string
>n : number