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

Implement partial type argument inference using the _ sigil #26349

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
67 changes: 60 additions & 7 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1990,6 +1990,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
var markerSubTypeForCheck = createTypeParameter();
markerSubTypeForCheck.constraint = markerSuperTypeForCheck;

var declaredSyntheticInferType = createTypeParameter();

var noTypePredicate = createTypePredicate(TypePredicateKind.Identifier, "<<unresolved>>", 0, anyType);

var anySignature = createSignature(/*declaration*/ undefined, /*typeParameters*/ undefined, /*thisParameter*/ undefined, emptyArray, anyType, /*resolvedTypePredicate*/ undefined, 0, SignatureFlags.None);
Expand Down Expand Up @@ -14863,7 +14865,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return undefined;
}

function getSignatureInstantiation(signature: Signature, typeArguments: Type[] | undefined, isJavascript: boolean, inferredTypeParameters?: readonly TypeParameter[]): Signature {
function getSignatureInstantiation(signature: Signature, typeArguments: readonly Type[] | undefined, isJavascript: boolean, inferredTypeParameters?: readonly TypeParameter[]): Signature {
const instantiatedSignature = getSignatureInstantiationWithoutFillingInTypeArguments(signature, fillMissingTypeArguments(typeArguments, signature.typeParameters, getMinTypeArgumentCount(signature.typeParameters), isJavascript));
if (inferredTypeParameters) {
const returnSignature = getSingleCallOrConstructSignature(getReturnTypeOfSignature(instantiatedSignature));
Expand Down Expand Up @@ -18485,6 +18487,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return node.flags & NodeFlags.JavaScriptFile && !noImplicitAny ? anyType : nonPrimitiveType;
case SyntaxKind.IntrinsicKeyword:
return intrinsicMarkerType;
case SyntaxKind.PlaceholderType:
return declaredSyntheticInferType;
case SyntaxKind.ThisType:
case SyntaxKind.ThisKeyword as TypeNodeSyntaxKind:
// TODO(rbuckton): `ThisKeyword` is no longer a `TypeNode`, but we defensively allow it here because of incorrect casts in the Language Service and because of `isPartOfTypeNode`.
Expand Down Expand Up @@ -32560,10 +32564,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return createTupleType(types, flags, inConstContext, length(names) === length(types) ? names : undefined);
}

function checkTypeArguments(signature: Signature, typeArgumentNodes: readonly TypeNode[], reportErrors: boolean, headMessage?: DiagnosticMessage): Type[] | undefined {
function checkTypeArguments(signature: Signature, typeArgumentNodes: readonly TypeNode[], reportErrors: boolean, headMessage?: DiagnosticMessage, typeArguments?: readonly Type[]): readonly Type[] | undefined {
const isJavascript = isInJSFile(signature.declaration);
const typeParameters = signature.typeParameters!;
const typeArgumentTypes = fillMissingTypeArguments(map(typeArgumentNodes, getTypeFromTypeNode), typeParameters, getMinTypeArgumentCount(typeParameters), isJavascript);
const typeArgumentTypes = typeArguments || fillMissingTypeArguments(map(typeArgumentNodes, getTypeFromTypeNode), typeParameters, getMinTypeArgumentCount(typeParameters), isJavascript);
if (some(typeArgumentTypes, isSyntheticInferType)) {
// Do validation once partial inference is complete
return typeArgumentTypes;
}
let mapper: TypeMapper | undefined;
for (let i = 0; i < typeArgumentNodes.length; i++) {
Debug.assert(typeParameters[i] !== undefined, "Should not call checkTypeArguments with too many type arguments");
Expand All @@ -32588,6 +32596,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return typeArgumentTypes;
}

function isSyntheticInferType(type: Type) {
return type === declaredSyntheticInferType;
}

function getJsxReferenceKind(node: JsxOpeningLikeElement): JsxReferenceKind {
if (isJsxIntrinsicTagName(node.tagName)) {
return JsxReferenceKind.Mixed;
Expand Down Expand Up @@ -33156,6 +33168,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
let candidatesForArgumentError: Signature[] | undefined;
let candidateForArgumentArityError: Signature | undefined;
let candidateForTypeArgumentError: Signature | undefined;
let candidateForTypeArgumentErrorTypeArguments: readonly Type[] | undefined;
let result: Signature | undefined;

// If we are in signature help, a trailing comma indicates that we intend to provide another argument,
Expand Down Expand Up @@ -33216,6 +33229,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
addRelatedInfo(d, createDiagnosticForNode(last.declaration, Diagnostics.The_last_overload_is_declared_here));
}
addImplementationSuccessElaboration(last, d);
if (typeArguments && some(typeArguments, n => n.kind === SyntaxKind.PlaceholderType)) {
const underscoreType = resolveName(node, "_" as __String, SymbolFlags.Type, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false);
if (underscoreType && underscoreType !== unknownSymbol) {
addRelatedInfo(d, createDiagnosticForNode(find(typeArguments, n => n.kind === SyntaxKind.PlaceholderType)!, Diagnostics.in_an_expression_type_argument_list_is_a_placeholder_type_If_you_meant_to_refer_to_the_type_named_write_0_instead));
}
}
diagnostics.add(d);
}
}
Expand Down Expand Up @@ -33273,7 +33292,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
diagnostics.add(getArgumentArityError(node, [candidateForArgumentArityError], args, headMessage));
}
else if (candidateForTypeArgumentError) {
checkTypeArguments(candidateForTypeArgumentError, (node as CallExpression | TaggedTemplateExpression | JsxOpeningLikeElement).typeArguments!, /*reportErrors*/ true, headMessage);
checkTypeArguments(candidateForTypeArgumentError, (node as CallExpression | TaggedTemplateExpression | JsxOpeningLikeElement).typeArguments!, /*reportErrors*/ true, headMessage, candidateForTypeArgumentErrorTypeArguments);
}
else {
const signaturesWithCorrectTypeArgumentArity = filter(signatures, s => hasCorrectTypeArgumentArity(s, typeArguments));
Expand All @@ -33292,6 +33311,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const oldCandidatesForArgumentError = candidatesForArgumentError;
const oldCandidateForArgumentArityError = candidateForArgumentArityError;
const oldCandidateForTypeArgumentError = candidateForTypeArgumentError;
const oldCandidateForTypeArgumentErrorTypeArguments = candidateForTypeArgumentErrorTypeArguments;

const failedSignatureDeclarations = failed.declaration?.symbol?.declarations || emptyArray;
const isOverload = failedSignatureDeclarations.length > 1;
Expand All @@ -33307,12 +33327,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
candidatesForArgumentError = oldCandidatesForArgumentError;
candidateForArgumentArityError = oldCandidateForArgumentArityError;
candidateForTypeArgumentError = oldCandidateForTypeArgumentError;
candidateForTypeArgumentErrorTypeArguments = oldCandidateForTypeArgumentErrorTypeArguments;
}

function chooseOverload(candidates: Signature[], relation: Map<string, RelationComparisonResult>, isSingleNonGenericCandidate: boolean, signatureHelpTrailingComma = false) {
candidatesForArgumentError = undefined;
candidateForArgumentArityError = undefined;
candidateForTypeArgumentError = undefined;
candidateForTypeArgumentErrorTypeArguments = undefined;

if (isSingleNonGenericCandidate) {
const candidate = candidates[0];
Expand All @@ -33335,12 +33357,43 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
let checkCandidate: Signature;
let inferenceContext: InferenceContext | undefined;

const isJavascript = isInJSFile(candidate.declaration);

if (candidate.typeParameters) {
let typeArgumentTypes: Type[] | undefined;
let typeArgumentTypes: readonly Type[] | undefined;
if (some(typeArguments)) {
typeArgumentTypes = checkTypeArguments(candidate, typeArguments, /*reportErrors*/ false);
let typeArgumentResult = checkTypeArguments(candidate, typeArguments, /*reportErrors*/ false);
if (typeArgumentResult) {
if (some(typeArgumentResult, isSyntheticInferType)) {
// There are implied inferences we must make, despite having type arguments
const originalParams = candidate.typeParameters;
const withOriginalArgs = map(typeArgumentResult, (r, i) => isSyntheticInferType(r) ? originalParams[i] : r);
const uninferedInstantiation = getSignatureInstantiation(candidate, withOriginalArgs, isJavascript);
inferenceContext = createInferenceContext(originalParams, uninferedInstantiation, isInJSFile(node) ? InferenceFlags.AnyDefault : InferenceFlags.None);
for (let i = 0; i < inferenceContext.inferences.length; i++) {
const correspondingArgument = typeArgumentResult[i];
if (!isSyntheticInferType(correspondingArgument)) {
const inference = inferenceContext.inferences[i];
inference.inferredType = correspondingArgument;
inference.isFixed = true;
inference.priority = 0;
}
}
typeArgumentResult = inferTypeArguments(node, uninferedInstantiation, args, argCheckMode | CheckMode.SkipGenericFunctions, inferenceContext);
argCheckMode |= inferenceContext.flags & InferenceFlags.SkippedGenericFunction ? CheckMode.SkipGenericFunctions : CheckMode.Normal;
}
}
if (typeArgumentResult) {
typeArgumentTypes = typeArgumentResult;
if (!checkTypeArguments(candidate, typeArguments, /*reportErrors*/ false, /*headMessage*/ undefined, typeArgumentTypes)) {
candidateForTypeArgumentError = candidate;
candidateForTypeArgumentErrorTypeArguments = typeArgumentTypes;
continue;
}
}
if (!typeArgumentTypes) {
candidateForTypeArgumentError = candidate;
candidateForTypeArgumentErrorTypeArguments = typeArgumentTypes;
continue;
}
}
Expand Down Expand Up @@ -33372,7 +33425,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
argCheckMode = checkMode & CheckMode.IsForStringLiteralArgumentCompletions;
if (inferenceContext) {
const typeArgumentTypes = inferTypeArguments(node, candidate, args, argCheckMode, inferenceContext);
checkCandidate = getSignatureInstantiation(candidate, typeArgumentTypes, isInJSFile(candidate.declaration), inferenceContext.inferredTypeParameters);
checkCandidate = getSignatureInstantiation(candidate, typeArgumentTypes, isJavascript, inferenceContext.inferredTypeParameters);
// If the original signature has a generic rest type, instantiation may produce a
// signature with different arity and we need to perform another arity check.
if (getNonArrayRestType(candidate) && !hasCorrectArity(node, args, checkCandidate, signatureHelpTrailingComma)) {
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1667,6 +1667,10 @@
"category": "Message",
"code": 2212
},
"`_` in an expression type argument list is a placeholder type. If you meant to refer to the type named `_`, write `[_][0]` instead.": {
"category": "Message",
"code": 2213
},

"Duplicate identifier '{0}'.": {
"category": "Error",
Expand Down
6 changes: 6 additions & 0 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1916,6 +1916,8 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri
return emitIntersectionType(node as IntersectionTypeNode);
case SyntaxKind.ConditionalType:
return emitConditionalType(node as ConditionalTypeNode);
case SyntaxKind.PlaceholderType:
return emitPlaceholderType();
case SyntaxKind.InferType:
return emitInferType(node as InferTypeNode);
case SyntaxKind.ParenthesizedType:
Expand Down Expand Up @@ -2836,6 +2838,10 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri
emit(node.falseType);
}

function emitPlaceholderType() {
write("_");
}

function emitInferType(node: InferTypeNode) {
writeKeyword("infer");
writeSpace();
Expand Down
9 changes: 9 additions & 0 deletions src/compiler/factory/nodeFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ import {
parseNodeFactory,
PartiallyEmittedExpression,
Path,
PlaceholderTypeNode,
PlusToken,
PostfixUnaryExpression,
PostfixUnaryOperator,
Expand Down Expand Up @@ -616,6 +617,7 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
updateConditionalTypeNode,
createInferTypeNode,
updateInferTypeNode,
createPlaceholderTypeNode,
createImportTypeNode,
updateImportTypeNode,
createParenthesizedType,
Expand Down Expand Up @@ -2457,6 +2459,13 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
: node;
}

// @api
function createPlaceholderTypeNode() {
const node = createBaseNode<PlaceholderTypeNode>(SyntaxKind.PlaceholderType);
node.transformFlags = TransformFlags.ContainsTypeScript;
return node;
}

// @api
function createInferTypeNode(typeParameter: TypeParameterDeclaration) {
const node = createBaseNode<InferTypeNode>(SyntaxKind.InferType);
Expand Down
27 changes: 23 additions & 4 deletions src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4844,6 +4844,25 @@ namespace Parser {
return finishNode(factory.createTypePredicateNode(assertsModifier, parameterName, type), pos);
}

function isEndOfTypeArgument(): boolean {
nextToken();
switch (token()) {
case SyntaxKind.CommaToken:
case SyntaxKind.GreaterThanToken:
return true;
}
return false;
}

function parsePlaceholderOrType(): TypeNode {
if (token() === SyntaxKind.Identifier && scanner.getTokenText() === "_" && lookAhead(isEndOfTypeArgument)) {
const pos = getNodePos();
nextToken();
return finishNode(factory.createPlaceholderTypeNode(), pos);
}
return parseType();
}

function parseType(): TypeNode {
if (contextFlags & NodeFlags.TypeExcludesFlags) {
return doOutsideOfContext(NodeFlags.TypeExcludesFlags, parseType);
Expand Down Expand Up @@ -6086,7 +6105,7 @@ namespace Parser {
return finishNode(factory.createJsxOpeningFragment(), pos);
}
const tagName = parseJsxElementName();
const typeArguments = (contextFlags & NodeFlags.JavaScriptFile) === 0 ? tryParseTypeArguments() : undefined;
const typeArguments = (contextFlags & NodeFlags.JavaScriptFile) === 0 ? tryParseTypeArguments(/*inferencePosition*/ true) : undefined;
const attributes = parseJsxAttributes();

let node: JsxOpeningLikeElement;
Expand Down Expand Up @@ -6452,7 +6471,7 @@ namespace Parser {
}
nextToken();

const typeArguments = parseDelimitedList(ParsingContext.TypeArguments, parseType);
const typeArguments = parseDelimitedList(ParsingContext.TypeArguments, parsePlaceholderOrType);
if (reScanGreaterToken() !== SyntaxKind.GreaterThanToken) {
// If it doesn't have the closing `>` then it's definitely not an type argument list.
return undefined;
Expand Down Expand Up @@ -8004,9 +8023,9 @@ namespace Parser {
return finishNode(factory.createExpressionWithTypeArguments(expression, typeArguments), pos);
}

function tryParseTypeArguments(): NodeArray<TypeNode> | undefined {
function tryParseTypeArguments(inferencePosition?: boolean): NodeArray<TypeNode> | undefined {
return token() === SyntaxKind.LessThanToken ?
parseBracketedList(ParsingContext.TypeArguments, parseType, SyntaxKind.LessThanToken, SyntaxKind.GreaterThanToken) : undefined;
parseBracketedList(ParsingContext.TypeArguments, inferencePosition ? parsePlaceholderOrType : parseType, SyntaxKind.LessThanToken, SyntaxKind.GreaterThanToken) : undefined;
}

function isHeritageClause(): boolean {
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/transformers/ts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,8 @@ export function transformTypeScript(context: TransformationContext) {
case SyntaxKind.IndexedAccessType:
case SyntaxKind.MappedType:
case SyntaxKind.LiteralType:
case SyntaxKind.PlaceholderType:
case SyntaxKind.InferType:
// TypeScript type nodes are elided.
// falls through

Expand Down
7 changes: 7 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ export const enum SyntaxKind {
NamedTupleMember,
TemplateLiteralType,
TemplateLiteralTypeSpan,
PlaceholderType,
ImportType,
// Binding patterns
ObjectBindingPattern,
Expand Down Expand Up @@ -741,6 +742,7 @@ export type TypeNodeSyntaxKind =
| SyntaxKind.JSDocNamepathType
| SyntaxKind.JSDocSignature
| SyntaxKind.JSDocTypeLiteral
| SyntaxKind.PlaceholderType
;

export type TokenSyntaxKind =
Expand Down Expand Up @@ -2162,6 +2164,10 @@ export interface TypeNode extends Node {
readonly kind: TypeNodeSyntaxKind;
}

export interface PlaceholderTypeNode extends TypeNode {
readonly kind: SyntaxKind.PlaceholderType;
}

export interface KeywordTypeNode<TKind extends KeywordTypeSyntaxKind = KeywordTypeSyntaxKind> extends KeywordToken<TKind>, TypeNode {
readonly kind: TKind;
}
Expand Down Expand Up @@ -8432,6 +8438,7 @@ export interface NodeFactory {
updateConditionalTypeNode(node: ConditionalTypeNode, checkType: TypeNode, extendsType: TypeNode, trueType: TypeNode, falseType: TypeNode): ConditionalTypeNode;
createInferTypeNode(typeParameter: TypeParameterDeclaration): InferTypeNode;
updateInferTypeNode(node: InferTypeNode, typeParameter: TypeParameterDeclaration): InferTypeNode;
createPlaceholderTypeNode(): PlaceholderTypeNode;
createImportTypeNode(argument: TypeNode, assertions?: ImportTypeAssertionContainer, qualifier?: EntityName, typeArguments?: readonly TypeNode[], isTypeOf?: boolean): ImportTypeNode;
updateImportTypeNode(node: ImportTypeNode, argument: TypeNode, assertions: ImportTypeAssertionContainer | undefined, qualifier: EntityName | undefined, typeArguments: readonly TypeNode[] | undefined, isTypeOf?: boolean): ImportTypeNode;
createParenthesizedType(type: TypeNode): ParenthesizedTypeNode;
Expand Down
Loading