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
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
@@ -715,6 +715,7 @@ import {
isRightSideOfQualifiedNameOrPropertyAccessOrJSDocMemberName,
isSameEntityName,
isSatisfiesExpression,
isSatisfiesKeyofExpression,
isSetAccessor,
isSetAccessorDeclaration,
isShorthandAmbientModuleSymbol,
@@ -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;
@@ -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);
}
}
}
@@ -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)) {
@@ -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));
}
@@ -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);
@@ -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;
@@ -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);
}
@@ -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;
}
@@ -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 {
@@ -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;
}
8 changes: 8 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
@@ -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
4 changes: 3 additions & 1 deletion src/compiler/parser.ts
Original file line number Diff line number Diff line change
@@ -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() {
27 changes: 22 additions & 5 deletions src/compiler/transformers/declarations.ts
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ import {
canProduceDiagnostics,
ClassDeclaration,
compact,
ComputedPropertyName,
concatenate,
ConditionalTypeNode,
ConstructorDeclaration,
@@ -128,6 +129,7 @@ import {
isOmittedExpression,
isPrimitiveLiteralValue,
isPrivateIdentifier,
isSatisfiesKeyofExpression,
isSemicolonClassElement,
isSetAccessorDeclaration,
isSourceFile,
@@ -147,6 +149,7 @@ import {
isVariableDeclaration,
isVarUsing,
LateBoundDeclaration,
LateBoundName,
LateVisibilityPaintedStatement,
length,
map,
@@ -159,6 +162,7 @@ import {
ModuleBody,
ModuleDeclaration,
ModuleName,
Mutable,
NamedDeclaration,
NamespaceDeclaration,
needsScopeMarker,
@@ -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;
@@ -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;
}
}
@@ -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;
@@ -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;
@@ -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) {
12 changes: 10 additions & 2 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
@@ -691,7 +691,8 @@ export type KeywordTypeSyntaxKind =
| SyntaxKind.SymbolKeyword
| SyntaxKind.UndefinedKeyword
| SyntaxKind.UnknownKeyword
| SyntaxKind.VoidKeyword;
| SyntaxKind.VoidKeyword
| SyntaxKind.KeyOfKeyword;

/** @internal */
export type TypeNodeSyntaxKind =
@@ -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 {
@@ -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 */
6 changes: 6 additions & 0 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
@@ -494,6 +494,7 @@ import {
ReturnStatement,
returnUndefined,
SatisfiesExpression,
SatisfiesKeyofEntityNameExpression,
ScriptKind,
ScriptTarget,
semanticDiagnosticsOptionDeclarations,
@@ -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