Skip to content

Commit 15bb626

Browse files
committed
Defer constraints for callback properties that reference a resolving declaration
A function-valued property whose un-annotated body names a declaration under resolution is treated like a getter for the constraint check only. Its contextual typing is untouched; deferring the typing itself breaks it.
1 parent 5c29a1a commit 15bb626

6 files changed

Lines changed: 855 additions & 1 deletion

File tree

tsc/internal/checker/inference.go

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1434,7 +1434,7 @@ func (c *Checker) hasObjectLiteralAccessors(t *Type, seen []*Type) bool {
14341434
return core.Some(c.getPropertiesOfObjectType(t), func(property *ast.Symbol) bool {
14351435
propertyType := c.valueSymbolLinks.Get(property).resolvedType
14361436
return property.Flags&ast.SymbolFlagsGetAccessor != 0 || c.getSpreadAccessorOrigin(property) != nil ||
1437-
c.isDeferredPropertyAssignment(property) ||
1437+
c.isDeferredPropertyAssignment(property) || c.isRecursiveCallbackProperty(property) ||
14381438
propertyType != nil && c.hasObjectLiteralAccessors(propertyType, seen)
14391439
})
14401440
}
@@ -1496,6 +1496,73 @@ func (c *Checker) isDeferredPropertyAssignment(symbol *ast.Symbol) bool {
14961496
return symbol.ValueDeclaration != nil && symbol.ValueDeclaration.Kind == ast.KindPropertyAssignment && c.deferredPropertyAssignments.Has(symbol.ValueDeclaration)
14971497
}
14981498

1499+
// A function-valued property whose un-annotated body refers to a declaration whose type is still
1500+
// being resolved. Checking the constraint would force the function's return type, which re-enters
1501+
// that resolution, so the property is deferred like a getter. Other callbacks keep the ordinary
1502+
// constraint check, so their contextual typing and constraint-failure recovery are unchanged.
1503+
func (c *Checker) isRecursiveCallbackProperty(property *ast.Symbol) bool {
1504+
decl := property.ValueDeclaration
1505+
if decl == nil {
1506+
return false
1507+
}
1508+
var fn *ast.Node
1509+
switch decl.Kind {
1510+
case ast.KindPropertyAssignment:
1511+
if init := decl.Initializer(); init != nil && ast.IsFunctionExpressionOrArrowFunction(init) {
1512+
fn = init
1513+
}
1514+
case ast.KindMethodDeclaration:
1515+
fn = decl
1516+
}
1517+
if fn == nil || fn.Type() != nil || fn.Body() == nil {
1518+
return false
1519+
}
1520+
return c.bodyReferencesResolvingDeclaration(fn.Body())
1521+
}
1522+
1523+
func (c *Checker) bodyReferencesResolvingDeclaration(body *ast.Node) bool {
1524+
var targets []*ast.Symbol
1525+
for i := range c.typeResolutions {
1526+
resolution := &c.typeResolutions[i]
1527+
if resolution.propertyName != TypeSystemPropertyNameType {
1528+
continue
1529+
}
1530+
if symbol, ok := resolution.target.(*ast.Symbol); ok && symbol.Flags&(ast.SymbolFlagsVariable|ast.SymbolFlagsProperty) != 0 {
1531+
targets = append(targets, symbol)
1532+
}
1533+
}
1534+
if len(targets) == 0 {
1535+
return false
1536+
}
1537+
var visit func(node *ast.Node) bool
1538+
visit = func(node *ast.Node) bool {
1539+
switch node.Kind {
1540+
case ast.KindIdentifier:
1541+
if ast.IsDeclarationName(node) && !ast.IsShorthandPropertyAssignment(node.Parent) {
1542+
return false
1543+
}
1544+
for _, symbol := range targets {
1545+
if symbol.Flags&ast.SymbolFlagsVariable != 0 && node.Text() == symbol.Name {
1546+
resolved := c.resolveName(node, symbol.Name, ast.SymbolFlagsValue|ast.SymbolFlagsExportValue, nil /*nameNotFoundMessage*/, false /*isUse*/, false /*excludeGlobals*/)
1547+
if c.getExportSymbolOfValueSymbolIfExported(resolved) == symbol {
1548+
return true
1549+
}
1550+
}
1551+
}
1552+
return false
1553+
case ast.KindPropertyAccessExpression:
1554+
for _, symbol := range targets {
1555+
if symbol.Flags&ast.SymbolFlagsProperty != 0 && node.Name().Text() == symbol.Name {
1556+
return true
1557+
}
1558+
}
1559+
return visit(node.Expression())
1560+
}
1561+
return node.ForEachChild(visit)
1562+
}
1563+
return visit(body)
1564+
}
1565+
14991566
func (c *Checker) getInferredTypes(n *InferenceContext) []*Type {
15001567
result := make([]*Type, len(n.inferences))
15011568
for i := range n.inferences {
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package fourslash_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/microsoft/TypeScript/tsc/internal/fourslash"
7+
"github.com/microsoft/TypeScript/tsc/internal/testutil"
8+
)
9+
10+
func TestHoverThenDiagnosticsRecursiveCallback(t *testing.T) {
11+
t.Parallel()
12+
for _, test := range []struct {
13+
name string
14+
first string
15+
}{
16+
{name: "diagnostics"},
17+
{name: "hover", first: "1"},
18+
{name: "call", first: "call"},
19+
{name: "output", first: "output"},
20+
} {
21+
t.Run(test.name, func(t *testing.T) {
22+
t.Parallel()
23+
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
24+
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, recursiveCallbackContent)
25+
defer done()
26+
const hover = `(property) parent: () => ZodOptional<ZodObject<{
27+
name: ZodString;
28+
parent: () => ZodOptional<ZodObject<...>>;
29+
}>>`
30+
const call = `function object<{
31+
name: ZodString;
32+
parent: () => ZodOptional<ZodObject<...>>;
33+
}>(shape: {
34+
name: ZodString;
35+
parent: () => ZodOptional<ZodObject<...>>;
36+
}): ZodObject<{
37+
name: ZodString;
38+
parent: () => ZodOptional<ZodObject<...>>;
39+
}>`
40+
const output = `const output: InferObjectType<{
41+
name: ZodString;
42+
parent: () => ZodOptional<ZodObject<...>>;
43+
}>`
44+
switch test.first {
45+
case "1":
46+
f.VerifyQuickInfoAt(t, "1", hover, "")
47+
case "call":
48+
f.VerifyQuickInfoAt(t, "call", call, "")
49+
case "output":
50+
f.VerifyQuickInfoAt(t, "output", output, "")
51+
}
52+
f.VerifyNoErrors(t)
53+
f.VerifyQuickInfoAt(t, "1", hover, "")
54+
f.VerifyQuickInfoAt(t, "call", call, "")
55+
f.VerifyQuickInfoAt(t, "output", output, "")
56+
f.VerifyNoErrors(t)
57+
})
58+
}
59+
}
60+
61+
const recursiveCallbackContent = `// @Filename: /tsconfig.json
62+
{ "compilerOptions": { "strict": true, "target": "esnext" } }
63+
// @Filename: /file.ts
64+
interface ZodType<T> {
65+
output: T;
66+
}
67+
68+
interface ZodString extends ZodType<string> {}
69+
70+
type ZodShape = Record<string, ZodType<any> | (() => ZodType<any>)>;
71+
type Resolve<T> = T extends () => infer R ? R : T;
72+
type InferObjectType<Shape extends ZodShape> = {
73+
[k in keyof Shape]: Resolve<Shape[k]>["output"];
74+
};
75+
interface ZodObject<T extends ZodShape> extends ZodType<InferObjectType<T>> {}
76+
interface ZodOptional<T extends ZodType<any>> extends ZodType<T["output"] | undefined> {}
77+
78+
declare function object<T extends ZodShape>(shape: T): ZodObject<T>;
79+
declare function string(): ZodString;
80+
declare function optional<T extends ZodType<any>>(schema: T): ZodOptional<T>;
81+
82+
const Category = /*call*/object({
83+
name: string(),
84+
parent/*1*/: () => optional(Category),
85+
});
86+
export const output/*output*/ = Category.output;`
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
recursiveCallbackPropertyConstraint.ts(19,7): error TS2322: Type 'string' is not assignable to type 'number'.
2+
recursiveCallbackPropertyConstraint.ts(20,25): error TS2339: Property 'missing' does not exist on type 'Output<{ name: Schema<string>; children: () => Schema<Output<any>[]>; }>'.
3+
recursiveCallbackPropertyConstraint.ts(30,25): error TS2339: Property 'missing' does not exist on type 'Output<{ name: Schema<string>; children(): Schema<Output<any>[]>; }>'.
4+
recursiveCallbackPropertyConstraint.ts(36,29): error TS2339: Property 'missing' does not exist on type 'Output<{ name: Schema<string>; posts: () => Schema<Output<{ title: Schema<string>; author: () => Schema<Output<any>>; }>[]>; }>'.
5+
recursiveCallbackPropertyConstraint.ts(41,40): error TS2322: Type 'number' is not assignable to type 'Schema<unknown> | (() => Schema<unknown>)'.
6+
recursiveCallbackPropertyConstraint.ts(45,22): error TS2322: Type '() => number' is not assignable to type 'string'.
7+
8+
9+
==== recursiveCallbackPropertyConstraint.ts (6 errors) ====
10+
interface Schema<T = unknown> {
11+
output: T;
12+
}
13+
14+
type Shape = Record<string, Schema | (() => Schema)>;
15+
type Resolve<T> = T extends () => infer R ? R : T;
16+
type Output<S extends Shape> = { [K in keyof S]: Resolve<S[K]> extends Schema<infer O> ? O : never };
17+
18+
declare function object<S extends Shape>(shape: S): Schema<Output<S>>;
19+
declare function array<S extends Schema>(schema: S): Schema<S["output"][]>;
20+
declare function text(): Schema<string>;
21+
22+
const node = object({
23+
name: text(),
24+
children: () => array(node),
25+
});
26+
27+
const name: string = node.output.children[0].children[0].name;
28+
const wrong: number = node.output.children[0].name;
29+
~~~~~
30+
!!! error TS2322: Type 'string' is not assignable to type 'number'.
31+
node.output.children[0].missing;
32+
~~~~~~~
33+
!!! error TS2339: Property 'missing' does not exist on type 'Output<{ name: Schema<string>; children: () => Schema<Output<any>[]>; }>'.
34+
35+
const tree = object({
36+
name: text(),
37+
children() {
38+
return array(tree);
39+
},
40+
});
41+
42+
const treeName: string = tree.output.children[0].children[0].name;
43+
tree.output.children[0].missing;
44+
~~~~~~~
45+
!!! error TS2339: Property 'missing' does not exist on type 'Output<{ name: Schema<string>; children(): Schema<Output<any>[]>; }>'.
46+
47+
export const user = object({ name: text(), posts: () => array(post) });
48+
const post = object({ title: text(), author: () => user });
49+
50+
const authorName: string = user.output.posts[0].author.posts[0].author.name;
51+
user.output.posts[0].author.missing;
52+
~~~~~~~
53+
!!! error TS2339: Property 'missing' does not exist on type 'Output<{ name: Schema<string>; posts: () => Schema<Output<{ title: Schema<string>; author: () => Schema<Output<any>>; }>[]>; }>'.
54+
55+
const mixed = object({ name: text(), self: () => array(mixed), other: () => text() });
56+
const otherName: string = mixed.output.self[0].other;
57+
58+
const invalid = object({ name: text(), bad: 42, self: () => array(invalid) });
59+
~~~
60+
!!! error TS2322: Type 'number' is not assignable to type 'Schema<unknown> | (() => Schema<unknown>)'.
61+
const invalidName: string = invalid.output.self[0].name;
62+
63+
declare function wrap<T extends Schema<string>>(value: T): T;
64+
const plain = wrap({ output: () => 42 });
65+
~~~~~~
66+
!!! error TS2322: Type '() => number' is not assignable to type 'string'.
67+
!!! related TS6500 recursiveCallbackPropertyConstraint.ts:2:5: The expected type comes from property 'output' which is declared here on type 'Schema<string>'
68+
const plainOutput: string = plain.output;
69+
70+
type LeafOutput = { name: string; children: LeafOutput[] };
71+
const leaf = object({ name: text(), children: (): Schema<LeafOutput[]> => array(leaf) });
72+
const leafName: string = leaf.output.children[0].name;
73+

0 commit comments

Comments
 (0)