Skip to content

Allow satisfies keyof assertions in computed property names #58829

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

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 72 additions & 8 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,7 @@ import {
isRightSideOfQualifiedNameOrPropertyAccessOrJSDocMemberName,
isSameEntityName,
isSatisfiesExpression,
isSatisfiesKeyofExpression,
isSetAccessor,
isSetAccessorDeclaration,
isShorthandAmbientModuleSymbol,
Expand Down Expand Up @@ -5863,7 +5864,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
entityName.parent.kind === SyntaxKind.TypeQuery ||
entityName.parent.kind === SyntaxKind.ExpressionWithTypeArguments && !isPartOfTypeNode(entityName.parent) ||
entityName.parent.kind === SyntaxKind.ComputedPropertyName ||
entityName.parent.kind === SyntaxKind.TypePredicate && (entityName.parent as TypePredicateNode).parameterName === entityName
entityName.parent.kind === SyntaxKind.TypePredicate && (entityName.parent as TypePredicateNode).parameterName === entityName ||
isSatisfiesKeyofExpression(entityName.parent)
) {
// Typeof value
meaning = SymbolFlags.Value | SymbolFlags.ExportValue;
Expand Down Expand Up @@ -7061,7 +7063,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}
else {
trackComputedName(decl.name.expression, saveEnclosingDeclaration, context);
trackComputedName(isEntityNameExpression(decl.name.expression) ? decl.name.expression : decl.name.expression.expression, saveEnclosingDeclaration, context);
}
}
}
Expand Down Expand Up @@ -7601,7 +7603,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return elideInitializerAndSetEmitFlags(node) as BindingName;
function elideInitializerAndSetEmitFlags(node: Node): Node {
if (context.tracker.canTrackSymbol && isComputedPropertyName(node) && isLateBindableName(node)) {
trackComputedName(node.expression, context.enclosingDeclaration, context);
trackComputedName(isEntityNameExpression(node.expression) ? node.expression : node.expression.expression, context.enclosingDeclaration, context);
}
let visited = visitEachChildWorker(node, elideInitializerAndSetEmitFlags, /*context*/ undefined, /*nodesVisitor*/ undefined, elideInitializerAndSetEmitFlags);
if (isBindingElement(visited)) {
Expand Down Expand Up @@ -13204,7 +13206,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (!isComputedPropertyName(node) && !isElementAccessExpression(node)) {
return false;
}
const expr = isComputedPropertyName(node) ? node.expression : node.argumentExpression;
let expr = isComputedPropertyName(node) ? node.expression : node.argumentExpression;
if (isSatisfiesExpression(expr) && expr.type.kind === SyntaxKind.KeyOfKeyword) {
expr = expr.expression;
}
return isEntityNameExpression(expr)
&& isTypeUsableAsPropertyName(isComputedPropertyName(node) ? checkComputedPropertyName(node) : checkExpressionCached(expr));
}
Expand Down Expand Up @@ -19623,6 +19628,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return type;
}

function getUniqueESSymbolTypeForSymbol(symbol: Symbol) {
const links = getSymbolLinks(symbol);
if (!links.uniqueESSymbolType) {
links.uniqueESSymbolType = createUniqueESSymbolType(symbol);
}
return links.uniqueESSymbolType;
}

function getESSymbolLikeTypeForNode(node: Node) {
if (isInJSFile(node) && isJSDocTypeExpression(node)) {
const host = getJSDocHost(node);
Expand All @@ -19633,8 +19646,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (isValidESSymbolDeclaration(node)) {
const symbol = isCommonJsExportPropertyAssignment(node) ? getSymbolOfNode((node as BinaryExpression).left) : getSymbolOfNode(node);
if (symbol) {
const links = getSymbolLinks(symbol);
return links.uniqueESSymbolType || (links.uniqueESSymbolType = createUniqueESSymbolType(symbol));
return getUniqueESSymbolTypeForSymbol(symbol);
}
}
return esSymbolType;
Expand Down Expand Up @@ -19706,6 +19718,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
addOptionality(getTypeFromTypeNode(node.type), /*isProperty*/ true, !!node.questionToken));
}

function getTypeFromKeyofKeywordTypeNode(node: KeywordTypeNode<SyntaxKind.KeyOfKeyword>) {
if (!isSatisfiesExpression(node.parent) || !isComputedPropertyName(node.parent.parent)) {
error(node, Diagnostics.keyof_type_must_have_an_operand_type);
}
return stringNumberSymbolType; // used to contextually type the LHS of the `satisfies` and ensures literals get literal types
}

function getTypeFromTypeNode(node: TypeNode): Type {
return getConditionalFlowTypeOfType(getTypeFromTypeNodeWorker(node), node);
}
Expand Down Expand Up @@ -19807,6 +19826,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
case SyntaxKind.PropertyAccessExpression as TypeNodeSyntaxKind:
const symbol = getSymbolAtLocation(node);
return symbol ? getDeclaredTypeOfSymbol(symbol) : errorType;
case SyntaxKind.KeyOfKeyword:
return getTypeFromKeyofKeywordTypeNode(node as KeywordTypeNode<SyntaxKind.KeyOfKeyword>);
default:
return errorType;
}
Expand Down Expand Up @@ -34552,7 +34573,22 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}
const indexedAccessType = getIndexedAccessTypeOrUndefined(objectType, effectiveIndexType, accessFlags, node) || errorType;
return checkIndexedAccessIndexType(getFlowTypeOfAccessExpression(node, getNodeLinks(node).resolvedSymbol, indexedAccessType, indexExpression, checkMode), node);
const result = checkIndexedAccessIndexType(getFlowTypeOfAccessExpression(node, getNodeLinks(node).resolvedSymbol, indexedAccessType, indexExpression, checkMode), node);
if (!isErrorType(result)) {
return result;
}
// lookup failed, try fallback without error reporting for more accurate type than `any`
if (isEntityNameExpression(indexExpression)) {
const fallback = getResolvedEntityNameUniqueSymbolType(indexExpression);
if (fallback) {
const indexedAccessType = getIndexedAccessTypeOrUndefined(objectType, fallback, accessFlags) || errorType;
// If the lookup simplifies/resolves, return it
if (!isErrorType(indexedAccessType) && !(indexedAccessType.flags & TypeFlags.IndexedAccess && (indexedAccessType as IndexedAccessType).objectType === objectType && (indexedAccessType as IndexedAccessType).indexType === fallback)) {
return getFlowTypeOfAccessExpression(node, getNodeLinks(node).resolvedSymbol, indexedAccessType, indexExpression, checkMode);
}
}
}
return result;
}

function callLikeExpressionMayHaveTypeArguments(node: CallLikeExpression): node is CallExpression | NewExpression | TaggedTemplateExpression | JsxOpeningElement {
Expand Down Expand Up @@ -37163,13 +37199,41 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return checkSatisfiesExpressionWorker(node.expression, node.type);
}

function getResolvedEntityNameUniqueSymbolType(expression: EntityNameExpression): Type | undefined {
const links = getNodeLinks(expression);
if (!links.uniqueSymbollFallback) {
let resolved = resolveEntityName(expression, SymbolFlags.Value | SymbolFlags.ExportValue, /*ignoreErrors*/ true);
if (!resolved || resolved === unknownSymbol) {
resolved = resolveEntityName(expression, SymbolFlags.Value | SymbolFlags.ExportValue, /*ignoreErrors*/ true, /*dontResolveAlias*/ true);
}
if (resolved) {
// overwrite to `unique symbol` type for reference, so it actually works as a property, despite the type error
links.uniqueSymbollFallback = getUniqueESSymbolTypeForSymbol(resolved);
}
else {
// reference does not resolve, do nothing
links.uniqueSymbollFallback = false;
}
}
return links.uniqueSymbollFallback || undefined;
}

function checkSatisfiesExpressionWorker(expression: Expression, target: TypeNode, checkMode?: CheckMode) {
const exprType = checkExpression(expression, checkMode);
const errorNode = findAncestor(target.parent, n => n.kind === SyntaxKind.SatisfiesExpression || n.kind === SyntaxKind.JSDocSatisfiesTag);
if (target.kind === SyntaxKind.KeyOfKeyword && isComputedPropertyName(expression.parent.parent)) {
if (!(exprType.flags & TypeFlags.StringOrNumberLiteralOrUnique)) {
error(expression, Diagnostics.A_satisfies_keyof_computed_property_name_must_be_exactly_a_single_string_number_or_unique_symbol_literal_type);
if (isEntityNameExpression(expression)) {
return getResolvedEntityNameUniqueSymbolType(expression) || exprType;
}
}
return exprType;
}
const targetType = getTypeFromTypeNode(target);
if (isErrorType(targetType)) {
return targetType;
}
const errorNode = findAncestor(target.parent, n => n.kind === SyntaxKind.SatisfiesExpression || n.kind === SyntaxKind.JSDocSatisfiesTag);
checkTypeAssignableToAndOptionallyElaborate(exprType, targetType, errorNode, expression, Diagnostics.Type_0_does_not_satisfy_the_expected_type_1);
return exprType;
}
Expand Down
8 changes: 8 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -7030,6 +7030,14 @@
"category": "Error",
"code": 9039
},
"`keyof` type must have an operand type.": {
"category": "Error",
"code": 9040
},
"A `satisfies keyof` computed property name must be exactly a single string, number, or unique symbol literal type.": {
"category": "Error",
"code": 9041
},
"JSX attributes must only be assigned a non-empty 'expression'.": {
"category": "Error",
"code": 17000
Expand Down
4 changes: 3 additions & 1 deletion src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4729,7 +4729,9 @@ namespace Parser {
function parseTypeOperator(operator: SyntaxKind.KeyOfKeyword | SyntaxKind.UniqueKeyword | SyntaxKind.ReadonlyKeyword) {
const pos = getNodePos();
parseExpected(operator);
return finishNode(factory.createTypeOperatorNode(operator, parseTypeOperatorOrHigher()), pos);
const arg = operator !== SyntaxKind.KeyOfKeyword || isStartOfType() ? parseTypeOperatorOrHigher() : undefined;
// parse `keyof` with no argument as a `keyof` keyword type node
return finishNode(arg ? factory.createTypeOperatorNode(operator, arg) : factory.createKeywordTypeNode(SyntaxKind.KeyOfKeyword), pos);
}

function tryParseConstraintOfInferType() {
Expand Down
27 changes: 22 additions & 5 deletions src/compiler/transformers/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
canProduceDiagnostics,
ClassDeclaration,
compact,
ComputedPropertyName,
concatenate,
ConditionalTypeNode,
ConstructorDeclaration,
Expand Down Expand Up @@ -128,6 +129,7 @@ import {
isOmittedExpression,
isPrimitiveLiteralValue,
isPrivateIdentifier,
isSatisfiesKeyofExpression,
isSemicolonClassElement,
isSetAccessorDeclaration,
isSourceFile,
Expand All @@ -147,6 +149,7 @@ import {
isVariableDeclaration,
isVarUsing,
LateBoundDeclaration,
LateBoundName,
LateVisibilityPaintedStatement,
length,
map,
Expand All @@ -159,6 +162,7 @@ import {
ModuleBody,
ModuleDeclaration,
ModuleName,
Mutable,
NamedDeclaration,
NamespaceDeclaration,
needsScopeMarker,
Expand Down Expand Up @@ -1000,7 +1004,7 @@ export function transformDeclarations(context: TransformationContext) {
if (isolatedDeclarations) {
// Classes and object literals usually elide properties with computed names that are not of a literal type
// In isolated declarations TSC needs to error on these as we don't know the type in a DTE.
if (!resolver.isDefinitelyReferenceToGlobalSymbolObject(input.name.expression)) {
if (!resolver.isDefinitelyReferenceToGlobalSymbolObject(input.name.expression) && !isSatisfiesKeyofExpression(input.name.expression)) {
if (isClassDeclaration(input.parent) || isObjectLiteralExpression(input.parent)) {
context.addDiagnostic(createDiagnosticForNode(input, Diagnostics.Computed_property_names_on_class_or_object_literals_cannot_be_inferred_with_isolatedDeclarations));
return;
Expand All @@ -1015,7 +1019,7 @@ export function transformDeclarations(context: TransformationContext) {
}
}
}
else if (!resolver.isLateBound(getParseTreeNode(input) as Declaration) || !isEntityNameExpression(input.name.expression)) {
else if (!resolver.isLateBound(getParseTreeNode(input) as Declaration) || !(isEntityNameExpression(input.name.expression) || isSatisfiesKeyofExpression(input.name.expression))) {
return;
}
}
Expand Down Expand Up @@ -1259,7 +1263,10 @@ export function transformDeclarations(context: TransformationContext) {

function cleanup<T extends Node>(returnValue: T | undefined): T | undefined {
if (returnValue && canProduceDiagnostic && hasDynamicName(input as Declaration)) {
checkName(input);
const updated = checkName(input, returnValue);
if (updated) {
returnValue = updated;
}
}
if (isEnclosingDeclaration(input)) {
enclosingDeclaration = previousEnclosingDeclaration;
Expand Down Expand Up @@ -1782,7 +1789,7 @@ export function transformDeclarations(context: TransformationContext) {
}
}

function checkName(node: DeclarationDiagnosticProducing) {
function checkName<T extends Node>(node: DeclarationDiagnosticProducing, returnValue: T | undefined): T | undefined {
let oldDiag: typeof getSymbolAccessibilityDiagnostic | undefined;
if (!suppressNewDiagnosticContexts) {
oldDiag = getSymbolAccessibilityDiagnostic;
Expand All @@ -1792,11 +1799,21 @@ export function transformDeclarations(context: TransformationContext) {
Debug.assert(hasDynamicName(node as NamedDeclaration)); // Should only be called with dynamic names
const decl = node as NamedDeclaration as LateBoundDeclaration;
const entityName = decl.name.expression;
checkEntityNameVisibility(entityName, enclosingDeclaration);
const nameExpr = isEntityNameExpression(entityName) ? entityName : entityName.expression;
checkEntityNameVisibility(nameExpr, enclosingDeclaration);
let result = returnValue;
if (returnValue && nameExpr !== entityName) {
const updated = factory.updateComputedPropertyName(decl.name, nameExpr) as LateBoundName;
result = factory.cloneNode(returnValue);
(result as Mutable<typeof result & LateBoundDeclaration>).name = updated as Extract<T, { name: ComputedPropertyName; }>["name"] & LateBoundName;
}
if (!suppressNewDiagnosticContexts) {
getSymbolAccessibilityDiagnostic = oldDiag!;
}
errorNameNode = undefined;
if (result as Node as T !== returnValue) {
return result as Node as T;
}
}

function shouldStripInternal(node: Node) {
Expand Down
12 changes: 10 additions & 2 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -691,7 +691,8 @@ export type KeywordTypeSyntaxKind =
| SyntaxKind.SymbolKeyword
| SyntaxKind.UndefinedKeyword
| SyntaxKind.UnknownKeyword
| SyntaxKind.VoidKeyword;
| SyntaxKind.VoidKeyword
| SyntaxKind.KeyOfKeyword;

/** @internal */
export type TypeNodeSyntaxKind =
Expand Down Expand Up @@ -1790,10 +1791,16 @@ export interface GeneratedPrivateIdentifier extends PrivateIdentifier {
readonly emitNode: EmitNode & { autoGenerate: AutoGenerateInfo; };
}

/** @internal */
export interface SatisfiesKeyofEntityNameExpression extends SatisfiesExpression {
readonly expression: EntityNameExpression;
readonly type: KeywordTypeNode<SyntaxKind.KeyOfKeyword>;
}

/** @internal */
// A name that supports late-binding (used in checker)
export interface LateBoundName extends ComputedPropertyName {
readonly expression: EntityNameExpression;
readonly expression: EntityNameExpression | SatisfiesKeyofEntityNameExpression;
}

export interface Decorator extends Node {
Expand Down Expand Up @@ -6176,6 +6183,7 @@ export interface NodeLinks {
fakeScopeForSignatureDeclaration?: "params" | "typeParams"; // If present, this is a fake scope injected into an enclosing declaration chain.
assertionExpressionType?: Type; // Cached type of the expression of a type assertion
externalHelpersModule?: Symbol; // Resolved symbol for the external helpers module
uniqueSymbollFallback?: Type | false;// Cached type of type node
}

/** @internal */
Expand Down
6 changes: 6 additions & 0 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,7 @@ import {
ReturnStatement,
returnUndefined,
SatisfiesExpression,
SatisfiesKeyofEntityNameExpression,
ScriptKind,
ScriptTarget,
semanticDiagnosticsOptionDeclarations,
Expand Down Expand Up @@ -11664,3 +11665,8 @@ export function hasInferredType(node: Node): node is HasInferredType {
return false;
}
}

/** @internal */
export function isSatisfiesKeyofExpression(node: Node): node is SatisfiesKeyofEntityNameExpression {
return node.kind === SyntaxKind.SatisfiesExpression && (node as SatisfiesExpression).type.kind === SyntaxKind.KeyOfKeyword && isEntityNameExpression((node as SatisfiesExpression).expression);
}
Loading