Skip to content
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
85 changes: 78 additions & 7 deletions tsc/internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -9049,7 +9049,12 @@ func (c *Checker) resolveCall(node *ast.Node, signatures []*Signature, candidate
return c.unknownSignature
}

s.args = c.getEffectiveCallArguments(node)
s.isSingleNonGenericCandidate = len(s.candidates) == 1 && len(s.candidates[0].typeParameters) == 0
var contextualSignature *Signature
if s.isSingleNonGenericCandidate {
contextualSignature = s.candidates[0]
}
s.args = c.getEffectiveCallArguments(node, contextualSignature)
// The excludeArgument array contains true for each context sensitive argument (an argument
// is context sensitive it is susceptible to a one-time permanent contextual typing).
//
Expand All @@ -9062,7 +9067,6 @@ func (c *Checker) resolveCall(node *ast.Node, signatures []*Signature, candidate
//
// For a decorator, no arguments are susceptible to contextual typing due to the fact
// decorators are applied to a declaration by the emitter, and not to an expression.
s.isSingleNonGenericCandidate = len(s.candidates) == 1 && len(s.candidates[0].typeParameters) == 0
if !isDecorator && !s.isSingleNonGenericCandidate && core.Some(s.args, c.isContextSensitive) {
s.argCheckMode = CheckModeSkipContextSensitive
} else {
Expand Down Expand Up @@ -9477,6 +9481,16 @@ func (c *Checker) isSignatureApplicable(node *ast.Node, args []*ast.Node, signat
for i := range argCount {
arg := args[i]
if !ast.IsOmittedExpression(arg) {
if ast.IsSpreadElement(arg) && i == len(args)-1 && isContextuallyTypableSpreadExpression(arg.Expression()) {
paramType := c.getRestTypeAtPosition(signature, i, false /*readonly*/)
argType := c.checkExpressionWithContextualType(arg.Expression(), paramType, nil /*inferenceContext*/, checkMode)
effectiveCheckArgumentNode := c.getEffectiveCheckNode(arg)
if !c.checkTypeRelatedToAndOptionallyElaborate(argType, paramType, relation, core.IfElse(reportErrors, effectiveCheckArgumentNode, nil), effectiveCheckArgumentNode, headMessage, diagnosticOutput) {
c.maybeAddMissingAwaitInfo(arg.Expression(), argType, paramType, relation, reportErrors, diagnosticOutput)
return false
}
continue
}
paramType := c.getTypeAtPosition(signature, i)
argType := c.checkExpressionWithContextualType(arg, paramType, nil /*inferenceContext*/, checkMode)
// If one or more arguments are still excluded (as indicated by CheckMode.SkipContextSensitive),
Expand Down Expand Up @@ -29781,6 +29795,8 @@ func (c *Checker) getContextualType(node *ast.Node, contextFlags ContextFlags) *
return c.getContextualTypeForObjectLiteralElement(parent, contextFlags)
case ast.KindSpreadAssignment:
return c.getContextualType(parent.Parent, contextFlags)
case ast.KindSpreadElement:
return c.getContextualTypeForSpreadElement(parent, contextFlags)
case ast.KindArrayLiteralExpression:
t := c.getApparentTypeOfContextualType(parent, contextFlags)
elementIndex := ast.IndexOfNode(parent.Elements(), node)
Expand Down Expand Up @@ -29866,7 +29882,7 @@ func (c *Checker) getContextuallyTypedParameterType(parameter *ast.Node) *Type {
}
iife := ast.GetImmediatelyInvokedFunctionExpression(fn)
if iife != nil {
args := c.getEffectiveCallArguments(iife)
args := c.getEffectiveCallArguments(iife, nil /*contextualSignature*/)
indexOfParameter := slices.Index(fn.Parameters(), parameter)
if hasDotDotDotToken(parameter) {
return c.getSpreadArgumentType(args, indexOfParameter, len(args), c.anyType, nil /*context*/, CheckModeNormal)
Expand Down Expand Up @@ -30164,7 +30180,7 @@ func (c *Checker) getContextualTypeForAwaitOperand(node *ast.Node, contextFlags

// In a typed function call, an argument or substitution expression is contextually typed by the type of the corresponding parameter.
func (c *Checker) getContextualTypeForArgument(callTarget *ast.Node, arg *ast.Node) *Type {
args := c.getEffectiveCallArguments(callTarget)
args := c.getEffectiveCallArguments(callTarget, nil /*contextualSignature*/)
argIndex := slices.Index(args, arg)
// -1 for e.g. the expression of a CallExpression, or the tag of a TaggedTemplateExpression
if argIndex == -1 {
Expand Down Expand Up @@ -30373,6 +30389,34 @@ func (c *Checker) getContextualTypeForObjectLiteralMethod(node *ast.Node, contex
return c.getContextualTypeForObjectLiteralElement(node, contextFlags)
}

// In a contextually typed array literal, a spread expression is contextually typed by the portion
// of the contextual array or tuple that the spread occupies.
func (c *Checker) getContextualTypeForSpreadElement(spread *ast.Node, contextFlags ContextFlags) *Type {
parent := spread.Parent
if ast.IsArrayLiteralExpression(parent) {
contextualType := c.getApparentTypeOfContextualType(parent, contextFlags)
if contextualType == nil {
return nil
}
index := ast.IndexOfNode(parent.Elements(), spread)
if index < 0 {
return nil
}
firstSpreadIndex, lastSpreadIndex := c.getSpreadIndices(parent)
return c.mapTypeEx(contextualType, func(t *Type) *Type {
if isTupleType(t) && firstSpreadIndex == lastSpreadIndex {
return c.sliceTupleType(t, index, len(parent.Elements())-index-1)
}
elementType := c.getContextualTypeForElementExpression(t, index, len(parent.Elements()), firstSpreadIndex, lastSpreadIndex)
if elementType == nil {
return nil
}
return c.createArrayType(elementType)
}, true /*noReductions*/)
}
return nil
}

func (c *Checker) getContextualTypeForElementExpression(t *Type, index int, length int, firstSpreadIndex int, lastSpreadIndex int) *Type {
if t == nil {
return nil
Expand Down Expand Up @@ -30443,7 +30487,7 @@ func (c *Checker) getContextualImportAttributeType(node *ast.Node) *Type {
}

// Returns the effective arguments for an expression that works like a function invocation.
func (c *Checker) getEffectiveCallArguments(node *ast.Node) []*ast.Node {
func (c *Checker) getEffectiveCallArguments(node *ast.Node, contextualSignature *Signature) []*ast.Node {
switch {
case ast.IsJsxOpeningFragment(node):
// This attributes Type does not include a children property yet, the same way a fragment created with <React.Fragment> does not at this stage
Expand Down Expand Up @@ -30480,9 +30524,12 @@ func (c *Checker) getEffectiveCallArguments(node *ast.Node) []*ast.Node {
for i := spreadIndex; i < len(args); i++ {
arg := args[i]
var spreadType *Type
// We can call checkExpressionCached because spread expressions never have a contextual type.
if ast.IsSpreadElement(arg) {
if len(c.flowLoopStack) != 0 {
if contextualSignature != nil && isContextuallyTypableSpreadExpression(arg.Expression()) {
argumentPosition := len(effectiveArgs)
contextualType := c.getRestTypeAtPosition(contextualSignature, argumentPosition, false /*readonly*/)
spreadType = c.checkExpressionWithContextualType(arg.Expression(), contextualType, nil /*inferenceContext*/, CheckModeNormal)
} else if len(c.flowLoopStack) != 0 {
spreadType = c.checkExpression(arg.Expression())
} else {
spreadType = c.checkExpressionCached(arg.Expression())
Expand All @@ -30509,6 +30556,30 @@ func (c *Checker) getEffectiveCallArguments(node *ast.Node) []*ast.Node {
}
}

// Returns true for expression trees that contain array literals whose element types can be
// improved by a contextual type. Other spread operands retain their existing context-free check.
func isContextuallyTypableSpreadExpression(node *ast.Node) bool {
switch node.Kind {
case ast.KindArrayLiteralExpression:
for _, element := range node.Elements() {
if ast.IsSpreadElement(element) && !isContextuallyTypableSpreadExpression(element.Expression()) {
return false
}
}
return true
case ast.KindParenthesizedExpression:
return isContextuallyTypableSpreadExpression(node.Expression())
case ast.KindConditionalExpression:
conditional := node.AsConditionalExpression()
return isContextuallyTypableSpreadExpression(conditional.WhenTrue) && isContextuallyTypableSpreadExpression(conditional.WhenFalse)
case ast.KindBinaryExpression:
binary := node.AsBinaryExpression()
return ast.NodeKindIs(binary.OperatorToken, ast.KindBarBarToken, ast.KindQuestionQuestionToken) &&
(isContextuallyTypableSpreadExpression(binary.Left) || isContextuallyTypableSpreadExpression(binary.Right))
}
return false
}

func (c *Checker) getSpreadArgumentIndex(args []*ast.Node) int {
return core.FindIndex(args, isSpreadArgument)
}
Expand Down
2 changes: 1 addition & 1 deletion tsc/internal/checker/utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ func (c *Checker) isOptionalParameter(node *ast.Node) bool {
parameterIndex := core.FindIndex(node.Parent.Parameters(), func(p *ast.ParameterDeclarationNode) bool { return p == node })
return node.Type() == nil &&
node.AsParameterDeclaration().DotDotDotToken == nil &&
parameterIndex >= len(c.getEffectiveCallArguments(iife))
parameterIndex >= len(c.getEffectiveCallArguments(iife, nil /*contextualSignature*/))
}
return false
}
Expand Down
10 changes: 5 additions & 5 deletions tsc/testdata/baselines/reference/compiler/awaitedType.types
Original file line number Diff line number Diff line change
Expand Up @@ -596,20 +596,20 @@ async function test40330() {
>async () => {} : () => Promise<void>

const res = await Promise.all([
>res : [number, ...void[]]
>await Promise.all([ promiseNumber, ...[promiseVoid()] ]) : [number, ...void[]]
>Promise.all([ promiseNumber, ...[promiseVoid()] ]) : Promise<[number, ...void[]]>
>res : [number, void]
>await Promise.all([ promiseNumber, ...[promiseVoid()] ]) : [number, void]
>Promise.all([ promiseNumber, ...[promiseVoid()] ]) : Promise<[number, void]>
>Promise.all : { <T>(values: Iterable<T | PromiseLike<T>>): Promise<Awaited<T>[]>; <T extends readonly unknown[] | []>(values: T): Promise<{ -readonly [P in keyof T]: Awaited<T[P]>; }>; }
>Promise : PromiseConstructor
>all : { <T>(values: Iterable<T | PromiseLike<T>>): Promise<Awaited<T>[]>; <T extends readonly unknown[] | []>(values: T): Promise<{ -readonly [P in keyof T]: Awaited<T[P]>; }>; }
>[ promiseNumber, ...[promiseVoid()] ] : [Promise<number>, ...Promise<void>[]]
>[ promiseNumber, ...[promiseVoid()] ] : [Promise<number>, Promise<void>]

promiseNumber,
>promiseNumber : Promise<number>

...[promiseVoid()]
>...[promiseVoid()] : Promise<void>
>[promiseVoid()] : Promise<void>[]
>[promiseVoid()] : [Promise<void>]
>promiseVoid() : Promise<void>
>promiseVoid : () => Promise<void>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
contextualTypingArraySpreadElements.ts(58,26): error TS2322: Type '{ field: "not-an-item"; }' is not assignable to type 'Item'.
Types of property 'field' are incompatible.
Type '"not-an-item"' is not assignable to type '"a" | "b" | "c"'.
contextualTypingArraySpreadElements.ts(59,13): error TS2345: Argument of type 'Item[] | { field: "not-an-item"; }[]' is not assignable to parameter of type 'Item[]'.
Type '{ field: "not-an-item"; }[]' is not assignable to type 'Item[]'.
Type '{ field: "not-an-item"; }' is not assignable to type 'Item'.
Types of property 'field' are incompatible.
Type '"not-an-item"' is not assignable to type '"a" | "b" | "c"'.
contextualTypingArraySpreadElements.ts(60,13): error TS2345: Argument of type 'Item[] | { field: "not-an-item"; }[]' is not assignable to parameter of type 'Item[]'.
Type '{ field: "not-an-item"; }[]' is not assignable to type 'Item[]'.
Type '{ field: "not-an-item"; }' is not assignable to type 'Item'.
Types of property 'field' are incompatible.
Type '"not-an-item"' is not assignable to type '"a" | "b" | "c"'.


==== contextualTypingArraySpreadElements.ts (3 errors) ====
interface Item {
field: "a" | "b" | "c";
}

interface First {
kind: "first";
}

interface Last {
kind: "last";
}

declare const condition: boolean;
declare const optionalItems: Item[] | undefined;
declare const nullableItems: Item[] | null;

const direct: Item[] = [
...[
{ field: "a" },
{ field: "b" },
],
];

const conditional: Item[] = [
...(condition ? [{ field: "a" }] : [{ field: "b" }]),
];

const tuple: [First, Item, Last] = [
{ kind: "first" },
...[{ field: "a" }],
{ kind: "last" },
];

const tupleFromSingleSpread: [First, Last] = [
...[
{ kind: "first" },
{ kind: "last" },
],
];

const multipleSpreads: Item[] = [
...[{ field: "a" }],
...[{ field: "b" }],
];

declare function acceptItems(...items: Item[]): void;
declare function acceptPair(first: First, last: Last): void;
declare function acceptTriple(first: First, item: Item, last: Last): void;

acceptItems(...[{ field: "a" }, { field: "b" }]);
acceptItems(...(condition ? [{ field: "a" }] : [{ field: "b" }]));
acceptPair(...[{ kind: "first" }, { kind: "last" }]);
acceptTriple(...[{ kind: "first" }, { field: "a" }], ...[{ kind: "last" }]);

acceptItems(...(optionalItems || [{ field: "a" }]));
acceptItems(...(nullableItems ?? [{ field: "b" }]));

const invalid: Item[] = [...[{ field: "not-an-item" }]];
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
!!! error TS2322: Type '{ field: "not-an-item"; }' is not assignable to type 'Item'.
!!! error TS2322: Types of property 'field' are incompatible.
!!! error TS2322: Type '"not-an-item"' is not assignable to type '"a" | "b" | "c"'.
acceptItems(...(optionalItems || [{ field: "not-an-item" }]));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
!!! error TS2345: Argument of type 'Item[] | { field: "not-an-item"; }[]' is not assignable to parameter of type 'Item[]'.
!!! error TS2345: Type '{ field: "not-an-item"; }[]' is not assignable to type 'Item[]'.
!!! error TS2345: Type '{ field: "not-an-item"; }' is not assignable to type 'Item'.
!!! error TS2345: Types of property 'field' are incompatible.
!!! error TS2345: Type '"not-an-item"' is not assignable to type '"a" | "b" | "c"'.
acceptItems(...(nullableItems ?? [{ field: "not-an-item" }]));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
!!! error TS2345: Argument of type 'Item[] | { field: "not-an-item"; }[]' is not assignable to parameter of type 'Item[]'.
!!! error TS2345: Type '{ field: "not-an-item"; }[]' is not assignable to type 'Item[]'.
!!! error TS2345: Type '{ field: "not-an-item"; }' is not assignable to type 'Item'.
!!! error TS2345: Types of property 'field' are incompatible.
!!! error TS2345: Type '"not-an-item"' is not assignable to type '"a" | "b" | "c"'.

Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//// [tests/cases/compiler/contextualTypingArraySpreadElements.ts] ////

//// [contextualTypingArraySpreadElements.ts]
interface Item {
field: "a" | "b" | "c";
}

interface First {
kind: "first";
}

interface Last {
kind: "last";
}

declare const condition: boolean;
declare const optionalItems: Item[] | undefined;
declare const nullableItems: Item[] | null;

const direct: Item[] = [
...[
{ field: "a" },
{ field: "b" },
],
];

const conditional: Item[] = [
...(condition ? [{ field: "a" }] : [{ field: "b" }]),
];

const tuple: [First, Item, Last] = [
{ kind: "first" },
...[{ field: "a" }],
{ kind: "last" },
];

const tupleFromSingleSpread: [First, Last] = [
...[
{ kind: "first" },
{ kind: "last" },
],
];

const multipleSpreads: Item[] = [
...[{ field: "a" }],
...[{ field: "b" }],
];

declare function acceptItems(...items: Item[]): void;
declare function acceptPair(first: First, last: Last): void;
declare function acceptTriple(first: First, item: Item, last: Last): void;

acceptItems(...[{ field: "a" }, { field: "b" }]);
acceptItems(...(condition ? [{ field: "a" }] : [{ field: "b" }]));
acceptPair(...[{ kind: "first" }, { kind: "last" }]);
acceptTriple(...[{ kind: "first" }, { field: "a" }], ...[{ kind: "last" }]);

acceptItems(...(optionalItems || [{ field: "a" }]));
acceptItems(...(nullableItems ?? [{ field: "b" }]));

const invalid: Item[] = [...[{ field: "not-an-item" }]];
acceptItems(...(optionalItems || [{ field: "not-an-item" }]));
acceptItems(...(nullableItems ?? [{ field: "not-an-item" }]));


//// [contextualTypingArraySpreadElements.js]
"use strict";
const direct = [
...[
{ field: "a" },
{ field: "b" },
],
];
const conditional = [
...(condition ? [{ field: "a" }] : [{ field: "b" }]),
];
const tuple = [
{ kind: "first" },
...[{ field: "a" }],
{ kind: "last" },
];
const tupleFromSingleSpread = [
...[
{ kind: "first" },
{ kind: "last" },
],
];
const multipleSpreads = [
...[{ field: "a" }],
...[{ field: "b" }],
];
acceptItems(...[{ field: "a" }, { field: "b" }]);
acceptItems(...(condition ? [{ field: "a" }] : [{ field: "b" }]));
acceptPair(...[{ kind: "first" }, { kind: "last" }]);
acceptTriple(...[{ kind: "first" }, { field: "a" }], ...[{ kind: "last" }]);
acceptItems(...(optionalItems || [{ field: "a" }]));
acceptItems(...(nullableItems ?? [{ field: "b" }]));
const invalid = [...[{ field: "not-an-item" }]];
acceptItems(...(optionalItems || [{ field: "not-an-item" }]));
acceptItems(...(nullableItems ?? [{ field: "not-an-item" }]));
Loading