Skip to content

feat(16656): as tuple type assertion #51463

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

Closed
wants to merge 1 commit into from
Closed
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
39 changes: 27 additions & 12 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ import {
isCatchClauseVariableDeclarationOrBindingElement, isCheckJsEnabledForFile, isChildOfNodeWithKind,
isClassDeclaration, isClassElement, isClassExpression, isClassLike, isClassStaticBlockDeclaration, isCommaSequence,
isCommonJsExportedExpression, isCommonJsExportPropertyAssignment, isComputedNonLiteralName, isComputedPropertyName,
isConstructorDeclaration, isConstructorTypeNode, isConstTypeReference, isDeclaration, isDeclarationName,
isConstructorDeclaration, isConstructorTypeNode, isConstTypeReference, isTupleTypeReference, isConstOrTupleTypeReference, isDeclaration, isDeclarationName,
isDeclarationReadonly, isDecorator, isDefaultedExpandoInitializer, isDeleteTarget, isDottedName, isDynamicName,
isEffectiveExternalModule, isElementAccessExpression, isEntityName, isEntityNameExpression, isEnumConst,
isEnumDeclaration, isEnumMember, isExclusivelyTypeOnlyImportOrExport, isExportAssignment, isExportDeclaration,
Expand Down Expand Up @@ -2038,6 +2038,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
|| (isJSDocTypeTag(location) && isConstTypeReference(location.typeExpression));
}

function isTupleAssertion(location: Node) {
return (isAssertionExpression(location) && isTupleTypeReference(location.type))
|| (isJSDocTypeTag(location) && isTupleTypeReference(location.typeExpression));
}

/**
* Resolve a given name for a given meaning at a given location. An error is reported if the name was not found and
* the nameNotFoundMessage argument is not undefined. Returns the resolved symbol, or undefined if no symbol with
Expand Down Expand Up @@ -2080,7 +2085,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
let isInExternalModule = false;

loop: while (location) {
if (name === "const" && isConstAssertion(location)) {
if (name === "const" && isConstAssertion(location) || name === "tuple" && isTupleAssertion(location)) {
// `const` in an `as const` has no symbol, but issues no error because there is no *actual* lookup of the type
// (it refers to the constant type of the expression instead)
return undefined;
Expand Down Expand Up @@ -14322,9 +14327,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const links = getNodeLinks(node);
if (!links.resolvedType) {
// handle LS queries on the `const` in `x as const` by resolving to the type of `x`
if (isConstTypeReference(node) && isAssertionExpression(node.parent)) {
if (isConstOrTupleTypeReference(node) && isAssertionExpression(node.parent)) {
links.resolvedSymbol = unknownSymbol;
return links.resolvedType = checkExpressionCached(node.parent.expression);
return links.resolvedType = checkExpressionCached(node.parent.expression, /*checkMode*/ undefined, /*forceTuple*/ isTupleTypeReference(node));
}
let symbol: Symbol | undefined;
let type: Type | undefined;
Expand Down Expand Up @@ -27856,7 +27861,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return getContextualTypeForArgument(parent as CallExpression | NewExpression, node);
case SyntaxKind.TypeAssertionExpression:
case SyntaxKind.AsExpression:
return isConstTypeReference((parent as AssertionExpression).type) ? tryFindWhenConstTypeReference(parent as AssertionExpression) : getTypeFromTypeNode((parent as AssertionExpression).type);
return isConstOrTupleTypeReference((parent as AssertionExpression).type) ? tryFindWhenConstOrTupleTypeReference(parent as AssertionExpression) : getTypeFromTypeNode((parent as AssertionExpression).type);
case SyntaxKind.BinaryExpression:
return getContextualTypeForBinaryOperand(node, contextFlags);
case SyntaxKind.PropertyAssignment:
Expand All @@ -27878,7 +27883,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// Like in `checkParenthesizedExpression`, an `/** @type {xyz} */` comment before a parenthesized expression acts as a type cast.
const tag = isInJSFile(parent) ? getJSDocTypeTag(parent) : undefined;
return !tag ? getContextualType(parent as ParenthesizedExpression, contextFlags) :
isJSDocTypeTag(tag) && isConstTypeReference(tag.typeExpression.type) ? tryFindWhenConstTypeReference(parent as ParenthesizedExpression) :
isJSDocTypeTag(tag) && isConstOrTupleTypeReference(tag.typeExpression.type) ? tryFindWhenConstOrTupleTypeReference(parent as ParenthesizedExpression) :
getTypeFromTypeNode(tag.typeExpression.type);
}
case SyntaxKind.NonNullExpression:
Expand All @@ -27898,7 +27903,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
return undefined;

function tryFindWhenConstTypeReference(node: Expression) {
function tryFindWhenConstOrTupleTypeReference(node: Expression) {
return getContextualType(node, contextFlags);
}
}
Expand Down Expand Up @@ -32761,7 +32766,17 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return false;
}

function isValidTupleAssertionArgument(node: Node): boolean {
return isArrayLiteralExpression(node) || isParenthesizedExpression(node) && isValidTupleAssertionArgument(node.expression);
}

function checkAssertionWorker(errNode: Node, type: TypeNode, expression: UnaryExpression | Expression, checkMode?: CheckMode) {
if (isTupleTypeReference(type)) {
if (!isValidTupleAssertionArgument(expression)) {
error(expression, Diagnostics.A_tuple_assertions_can_only_be_applied_to_array_literals);
}
return getRegularTypeOfLiteralType(checkExpression(expression, checkMode, /*forceTuple*/ true));
}
let exprType = checkExpression(expression, checkMode);
if (isConstTypeReference(type)) {
if (!isValidConstAssertionArgument(expression)) {
Expand Down Expand Up @@ -35183,9 +35198,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}

function checkExpressionCached(node: Expression | QualifiedName, checkMode?: CheckMode): Type {
function checkExpressionCached(node: Expression | QualifiedName, checkMode?: CheckMode, forceTuple?: boolean): Type {
if (checkMode) {
return checkExpression(node, checkMode);
return checkExpression(node, checkMode, forceTuple);
}
const links = getNodeLinks(node);
if (!links.resolvedType) {
Expand All @@ -35196,7 +35211,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const saveFlowTypeCache = flowTypeCache;
flowLoopStart = flowLoopCount;
flowTypeCache = undefined;
links.resolvedType = checkExpression(node, checkMode);
links.resolvedType = checkExpression(node, checkMode, forceTuple);
flowTypeCache = saveFlowTypeCache;
flowLoopStart = saveFlowLoopStart;
}
Expand Down Expand Up @@ -35508,7 +35523,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
let expr = skipParentheses(node, /*excludeJSDocTypeAssertions*/ true);
if (isJSDocTypeAssertion(expr)) {
const type = getJSDocTypeAssertionType(expr);
if (!isConstTypeReference(type)) {
if (!isConstOrTupleTypeReference(type)) {
return getTypeFromTypeNode(type);
}
}
Expand All @@ -35522,7 +35537,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return type;
}
}
else if (isAssertionExpression(expr) && !isConstTypeReference(expr.type)) {
else if (isAssertionExpression(expr) && !isConstOrTupleTypeReference(expr.type)) {
return getTypeFromTypeNode((expr as TypeAssertion).type);
}
else if (node.kind === SyntaxKind.NumericLiteral || node.kind === SyntaxKind.StringLiteral ||
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1549,6 +1549,10 @@
"category": "Message",
"code": 1483
},
"A 'tuple' assertions can only be applied to array literals.": {
"category": "Error",
"code": 1484
},

"The types of '{0}' are incompatible between these types.": {
"category": "Error",
Expand Down
1 change: 1 addition & 0 deletions src/compiler/factory/nodeFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1123,6 +1123,7 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
case SyntaxKind.StringKeyword:
case SyntaxKind.BooleanKeyword:
case SyntaxKind.SymbolKeyword:
case SyntaxKind.TupleKeyword:
case SyntaxKind.VoidKeyword:
case SyntaxKind.UnknownKeyword:
case SyntaxKind.UndefinedKeyword: // `undefined` is an Identifier in the expression case.
Expand Down
1 change: 1 addition & 0 deletions src/compiler/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export const textToKeywordObj: MapLike<KeywordSyntaxKind> = {
throw: SyntaxKind.ThrowKeyword,
true: SyntaxKind.TrueKeyword,
try: SyntaxKind.TryKeyword,
tuple: SyntaxKind.TupleKeyword,
type: SyntaxKind.TypeKeyword,
typeof: SyntaxKind.TypeOfKeyword,
undefined: SyntaxKind.UndefinedKeyword,
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ export const enum SyntaxKind {
SetKeyword,
StringKeyword,
SymbolKeyword,
TupleKeyword,
TypeKeyword,
UndefinedKeyword,
UniqueKeyword,
Expand Down Expand Up @@ -627,6 +628,7 @@ export type KeywordSyntaxKind =
| SyntaxKind.ThrowKeyword
| SyntaxKind.TrueKeyword
| SyntaxKind.TryKeyword
| SyntaxKind.TupleKeyword
| SyntaxKind.TypeKeyword
| SyntaxKind.TypeOfKeyword
| SyntaxKind.UndefinedKeyword
Expand Down
9 changes: 9 additions & 0 deletions src/compiler/utilitiesPublic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,15 @@ export function isConstTypeReference(node: Node) {
node.typeName.escapedText === "const" && !node.typeArguments;
}

export function isTupleTypeReference(node: Node) {
return isTypeReferenceNode(node) && isIdentifier(node.typeName) &&
node.typeName.escapedText === "tuple" && !node.typeArguments;
}

export function isConstOrTupleTypeReference(node: Node) {
return isConstTypeReference(node) || isTupleTypeReference(node);
}

export function skipPartiallyEmittedExpressions(node: Expression): Expression;
export function skipPartiallyEmittedExpressions(node: Node): Node;
export function skipPartiallyEmittedExpressions(node: Node) {
Expand Down
3 changes: 3 additions & 0 deletions src/harness/fourslashInterfaceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1234,6 +1234,9 @@ export namespace Completion {
export const typeAssertionKeywords: readonly ExpectedCompletionEntry[] =
globalTypesPlus([keywordEntry("const")]);

export const typeAssertionKeywordsAfterArray: readonly ExpectedCompletionEntry[] =
globalTypesPlus([keywordEntry("const"), keywordEntry("tuple")]);

function getInJsKeywords(keywords: readonly ExpectedCompletionEntryObject[]): readonly ExpectedCompletionEntryObject[] {
return keywords.filter(keyword => {
switch (keyword.name) {
Expand Down
4 changes: 2 additions & 2 deletions src/services/classifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
ClassificationResult, Classifications, ClassificationType, ClassificationTypeNames, ClassifiedSpan, Classifier,
commentPragmas, couldStartTrivia, createScanner, createTextSpan, Debug, decodedTextSpanIntersectsWith,
EndOfLineState, EnumDeclaration, getMeaningFromLocation, getModuleInstanceState, getTypeArgumentOrTypeParameterList,
HasJSDoc, InterfaceDeclaration, isAccessibilityModifier, isConstTypeReference, isIdentifier, isJSDoc, isKeyword,
HasJSDoc, InterfaceDeclaration, isAccessibilityModifier, isConstOrTupleTypeReference, isIdentifier, isJSDoc, isKeyword,
isLineBreak, isModuleDeclaration, isPunctuation, isTemplateLiteralKind, isThisIdentifier, isToken, isTrivia, JSDoc,
JSDocAugmentsTag, JSDocCallbackTag, JSDocEnumTag, JSDocImplementsTag, JSDocParameterTag, JSDocPropertyTag,
JSDocReturnTag, JSDocSeeTag, JSDocTemplateTag, JSDocThisTag, JSDocTypedefTag, JSDocTypeTag, JsxAttribute,
Expand Down Expand Up @@ -1092,7 +1092,7 @@ export function getEncodedSyntacticClassifications(cancellationToken: Cancellati
return;
}

if (isConstTypeReference(token.parent)) {
if (isConstOrTupleTypeReference(token.parent)) {
return ClassificationType.keyword;
}
}
Expand Down
10 changes: 8 additions & 2 deletions src/services/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ import {
SymbolFlags, SymbolId, SyntaxKind, TextChange, textChanges, textPart, TextRange, TextSpan, timestamp, Token,
TokenSyntaxKind, tokenToString, tryCast, tryGetImportFromModuleSpecifier, Type, TypeChecker, TypeElement, TypeFlags,
typeHasCallOrConstructSignatures, TypeLiteralNode, TypeOnlyAliasDeclaration, unescapeLeadingUnderscores,
UnionReduction, UnionType, UserPreferences, VariableDeclaration, walkUpParenthesizedExpressions,
UnionReduction, UnionType, UserPreferences, VariableDeclaration, walkUpParenthesizedExpressions, isArrayLiteralExpression,
} from "./_namespaces/ts";
import { StringCompletions } from "./_namespaces/ts.Completions";

Expand Down Expand Up @@ -240,6 +240,7 @@ const enum KeywordCompletionFilters {
ConstructorParameterKeywords, // Keywords at constructor parameter
FunctionLikeBodyKeywords, // Keywords at function like body
TypeAssertionKeywords,
TypeAssertionKeywordsAfterArray,
TypeKeywords,
TypeKeyword, // Literally just `type`
Last = TypeKeyword
Expand Down Expand Up @@ -2693,7 +2694,9 @@ function getCompletionData(
collectAutoImports();
if (isTypeOnlyLocation) {
keywordFilters = contextToken && isAssertionExpression(contextToken.parent)
? KeywordCompletionFilters.TypeAssertionKeywords
? isArrayLiteralExpression(walkUpParenthesizedExpressions(contextToken.parent.expression))
? KeywordCompletionFilters.TypeAssertionKeywordsAfterArray
: KeywordCompletionFilters.TypeAssertionKeywords
: KeywordCompletionFilters.TypeKeywords;
}
}
Expand Down Expand Up @@ -3985,6 +3988,8 @@ function getTypescriptKeywordCompletions(keywordFilter: KeywordCompletionFilters
return isParameterPropertyModifier(kind);
case KeywordCompletionFilters.TypeAssertionKeywords:
return isTypeKeyword(kind) || kind === SyntaxKind.ConstKeyword;
case KeywordCompletionFilters.TypeAssertionKeywordsAfterArray:
return isTypeKeyword(kind) || kind === SyntaxKind.ConstKeyword || kind === SyntaxKind.TupleKeyword;
case KeywordCompletionFilters.TypeKeywords:
return isTypeKeyword(kind);
case KeywordCompletionFilters.TypeKeyword:
Expand Down Expand Up @@ -4021,6 +4026,7 @@ function isTypeScriptOnlyKeyword(kind: SyntaxKind) {
case SyntaxKind.ReadonlyKeyword:
case SyntaxKind.StringKeyword:
case SyntaxKind.SymbolKeyword:
case SyntaxKind.TupleKeyword:
case SyntaxKind.TypeKeyword:
case SyntaxKind.UniqueKeyword:
case SyntaxKind.UnknownKeyword:
Expand Down
4 changes: 2 additions & 2 deletions src/services/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
hasJSDocNodes, hasProperty, hasStaticModifier, hasSyntacticModifier, HighlightSpanKind, HostCancellationToken,
hostGetCanonicalFileName, hostUsesCaseSensitiveFileNames, Identifier, identity, idText, ImplementationLocation,
ImportDeclaration, IndexKind, IndexType, InlayHint, InlayHints, InlayHintsContext, insertSorted, InterfaceType,
IntersectionType, isArray, isBindingPattern, isComputedPropertyName, isConstTypeReference, IScriptSnapshot,
IntersectionType, isArray, isBindingPattern, isComputedPropertyName, isConstOrTupleTypeReference, IScriptSnapshot,
isDeclarationName, isGetAccessor, isIdentifier, isImportMeta, isInComment, isInsideJsxElement,
isInsideJsxElementOrAttribute, isInString, isInTemplateString, isIntrinsicJsxName, isJSDocCommentContainingNode,
isJsxAttributes, isJsxClosingElement, isJsxElement, isJsxFragment, isJsxOpeningElement, isJsxOpeningFragment,
Expand Down Expand Up @@ -1795,7 +1795,7 @@ export function createLanguageService(
function shouldGetType(sourceFile: SourceFile, node: Node, position: number): boolean {
switch (node.kind) {
case SyntaxKind.Identifier:
return !isLabelName(node) && !isTagName(node) && !isConstTypeReference(node.parent);
return !isLabelName(node) && !isTagName(node) && !isConstOrTupleTypeReference(node.parent);
case SyntaxKind.PropertyAccessExpression:
case SyntaxKind.QualifiedName:
// Don't return quickInfo if inside the comment in `a/**/.b`
Expand Down
4 changes: 2 additions & 2 deletions src/services/symbolDisplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
getMeaningFromLocation, getNameOfDeclaration, getNodeModifiers, getObjectFlags, getParseTreeNode,
getSourceFileOfNode, getTextOfConstantValue, getTextOfIdentifierOrLiteral, getTextOfNode, hasSyntacticModifier,
idText, ImportEqualsDeclaration, isArrowFunction, isBindingElement, isCallExpression, isCallExpressionTarget,
isCallOrNewExpression, isClassExpression, isConstTypeReference, isDeprecatedDeclaration, isEnumConst,
isCallOrNewExpression, isClassExpression, isConstOrTupleTypeReference, isDeprecatedDeclaration, isEnumConst,
isEnumDeclaration, isExpression, isExternalModuleImportEqualsDeclaration, isFirstDeclarationOfSymbolParameter,
isFunctionBlock, isFunctionExpression, isFunctionLike, isFunctionLikeKind, isIdentifier, isInExpressionContext,
isJsxOpeningLikeElement, isLet, isModuleWithStringLiteralName, isNameOfFunctionDeclaration, isNewExpressionTarget,
Expand Down Expand Up @@ -364,7 +364,7 @@ export function getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker: Typ
displayParts.push(spacePart());
displayParts.push(operatorPart(SyntaxKind.EqualsToken));
displayParts.push(spacePart());
addRange(displayParts, typeToDisplayParts(typeChecker, isConstTypeReference(location.parent) ? typeChecker.getTypeAtLocation(location.parent) : typeChecker.getDeclaredTypeOfSymbol(symbol), enclosingDeclaration, TypeFormatFlags.InTypeAlias));
addRange(displayParts, typeToDisplayParts(typeChecker, isConstOrTupleTypeReference(location.parent) ? typeChecker.getTypeAtLocation(location.parent) : typeChecker.getDeclaredTypeOfSymbol(symbol), enclosingDeclaration, TypeFormatFlags.InTypeAlias));
}
if (symbolFlags & SymbolFlags.Enum) {
prefixNextMeaning();
Expand Down
Loading