Skip to content

Commit 2533d82

Browse files
authored
Make a fresh empty object literal not a subtype of a type with an index signaure (#29975)
* Forbid inferable index checkign during subtype relationship checking * Merge object.values and object.entries overloads to work around subtype change * Invert subtype relationship between fresh empty objects and non-empty object types * Remvoe comment * Revert lib change * Remove trailing whitespace ffs
1 parent 4db4c58 commit 2533d82

11 files changed

+747
-0
lines changed

src/compiler/checker.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12787,6 +12787,11 @@ namespace ts {
1278712787
else if (isReadonlyArrayType(target) ? isArrayType(source) || isTupleType(source) : isArrayType(target) && isTupleType(source) && !source.target.readonly) {
1278812788
return isRelatedTo(getIndexTypeOfType(source, IndexKind.Number) || anyType, getIndexTypeOfType(target, IndexKind.Number) || anyType, reportErrors);
1278912789
}
12790+
// Consider a fresh empty object literal type "closed" under the subtype relationship - this way `{} <- {[idx: string]: any} <- fresh({})`
12791+
// and not `{} <- fresh({}) <- {[idx: string]: any}`
12792+
else if (relation === subtypeRelation && isEmptyObjectType(target) && getObjectFlags(target) & ObjectFlags.FreshLiteral && !isEmptyObjectType(source)) {
12793+
return Ternary.False;
12794+
}
1279012795
// Even if relationship doesn't hold for unions, intersections, or generic type references,
1279112796
// it may hold in a structural comparison.
1279212797
// In a check of the form X = A & B, we will have previously checked if A relates to X or B relates
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
tests/cases/compiler/emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts(41,3): error TS2322: Type 'Dictionary<string>' is not assignable to type 'Record<string, Bar>'.
2+
Index signatures are incompatible.
3+
Type 'string' is not assignable to type 'Bar'.
4+
5+
6+
==== tests/cases/compiler/emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts (1 errors) ====
7+
// This should behave the same as emptyObjectNotSubtypeOfIndexSignatureContainingObject2.ts
8+
// Begin types from Lodash.
9+
interface Dictionary<T> {
10+
[index: string]: T;
11+
}
12+
13+
interface NumericDictionary<T> {
14+
[index: number]: T;
15+
}
16+
17+
type ObjectIterator<TObject, TResult> = (
18+
value: TObject[keyof TObject],
19+
key: string,
20+
collection: TObject
21+
) => TResult;
22+
23+
type DictionaryIterator<T, TResult> = ObjectIterator<Dictionary<T>, TResult>;
24+
25+
// In lodash.d.ts this function has many overloads, but this seems to be the problematic one.
26+
function mapValues<T, TResult>(
27+
obj: Dictionary<T> | NumericDictionary<T> | null | undefined,
28+
callback: DictionaryIterator<T, TResult>
29+
): Dictionary<TResult> {
30+
return null as any;
31+
}
32+
// End types from Lodash.
33+
34+
interface Foo {
35+
foo: string;
36+
}
37+
38+
interface Bar {
39+
bar: string;
40+
}
41+
42+
export function fooToBar(
43+
foos: Record<string, Foo>
44+
): Record<string, Bar | null> {
45+
const result = foos == null ? {} : mapValues(foos, f => f.foo);
46+
// This line _should_ fail, because `result` is not the right type.
47+
return result;
48+
~~~~~~~~~~~~~~
49+
!!! error TS2322: Type 'Dictionary<string>' is not assignable to type 'Record<string, Bar>'.
50+
!!! error TS2322: Index signatures are incompatible.
51+
!!! error TS2322: Type 'string' is not assignable to type 'Bar'.
52+
}
53+
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
//// [emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts]
2+
// This should behave the same as emptyObjectNotSubtypeOfIndexSignatureContainingObject2.ts
3+
// Begin types from Lodash.
4+
interface Dictionary<T> {
5+
[index: string]: T;
6+
}
7+
8+
interface NumericDictionary<T> {
9+
[index: number]: T;
10+
}
11+
12+
type ObjectIterator<TObject, TResult> = (
13+
value: TObject[keyof TObject],
14+
key: string,
15+
collection: TObject
16+
) => TResult;
17+
18+
type DictionaryIterator<T, TResult> = ObjectIterator<Dictionary<T>, TResult>;
19+
20+
// In lodash.d.ts this function has many overloads, but this seems to be the problematic one.
21+
function mapValues<T, TResult>(
22+
obj: Dictionary<T> | NumericDictionary<T> | null | undefined,
23+
callback: DictionaryIterator<T, TResult>
24+
): Dictionary<TResult> {
25+
return null as any;
26+
}
27+
// End types from Lodash.
28+
29+
interface Foo {
30+
foo: string;
31+
}
32+
33+
interface Bar {
34+
bar: string;
35+
}
36+
37+
export function fooToBar(
38+
foos: Record<string, Foo>
39+
): Record<string, Bar | null> {
40+
const result = foos == null ? {} : mapValues(foos, f => f.foo);
41+
// This line _should_ fail, because `result` is not the right type.
42+
return result;
43+
}
44+
45+
46+
//// [emptyObjectNotSubtypeOfIndexSignatureContainingObject1.js]
47+
"use strict";
48+
exports.__esModule = true;
49+
// In lodash.d.ts this function has many overloads, but this seems to be the problematic one.
50+
function mapValues(obj, callback) {
51+
return null;
52+
}
53+
function fooToBar(foos) {
54+
var result = foos == null ? {} : mapValues(foos, function (f) { return f.foo; });
55+
// This line _should_ fail, because `result` is not the right type.
56+
return result;
57+
}
58+
exports.fooToBar = fooToBar;
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
=== tests/cases/compiler/emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts ===
2+
// This should behave the same as emptyObjectNotSubtypeOfIndexSignatureContainingObject2.ts
3+
// Begin types from Lodash.
4+
interface Dictionary<T> {
5+
>Dictionary : Symbol(Dictionary, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 0, 0))
6+
>T : Symbol(T, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 2, 21))
7+
8+
[index: string]: T;
9+
>index : Symbol(index, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 3, 3))
10+
>T : Symbol(T, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 2, 21))
11+
}
12+
13+
interface NumericDictionary<T> {
14+
>NumericDictionary : Symbol(NumericDictionary, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 4, 1))
15+
>T : Symbol(T, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 6, 28))
16+
17+
[index: number]: T;
18+
>index : Symbol(index, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 7, 3))
19+
>T : Symbol(T, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 6, 28))
20+
}
21+
22+
type ObjectIterator<TObject, TResult> = (
23+
>ObjectIterator : Symbol(ObjectIterator, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 8, 1))
24+
>TObject : Symbol(TObject, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 10, 20))
25+
>TResult : Symbol(TResult, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 10, 28))
26+
27+
value: TObject[keyof TObject],
28+
>value : Symbol(value, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 10, 41))
29+
>TObject : Symbol(TObject, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 10, 20))
30+
>TObject : Symbol(TObject, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 10, 20))
31+
32+
key: string,
33+
>key : Symbol(key, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 11, 32))
34+
35+
collection: TObject
36+
>collection : Symbol(collection, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 12, 14))
37+
>TObject : Symbol(TObject, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 10, 20))
38+
39+
) => TResult;
40+
>TResult : Symbol(TResult, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 10, 28))
41+
42+
type DictionaryIterator<T, TResult> = ObjectIterator<Dictionary<T>, TResult>;
43+
>DictionaryIterator : Symbol(DictionaryIterator, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 14, 13))
44+
>T : Symbol(T, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 16, 24))
45+
>TResult : Symbol(TResult, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 16, 26))
46+
>ObjectIterator : Symbol(ObjectIterator, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 8, 1))
47+
>Dictionary : Symbol(Dictionary, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 0, 0))
48+
>T : Symbol(T, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 16, 24))
49+
>TResult : Symbol(TResult, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 16, 26))
50+
51+
// In lodash.d.ts this function has many overloads, but this seems to be the problematic one.
52+
function mapValues<T, TResult>(
53+
>mapValues : Symbol(mapValues, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 16, 77))
54+
>T : Symbol(T, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 19, 19))
55+
>TResult : Symbol(TResult, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 19, 21))
56+
57+
obj: Dictionary<T> | NumericDictionary<T> | null | undefined,
58+
>obj : Symbol(obj, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 19, 31))
59+
>Dictionary : Symbol(Dictionary, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 0, 0))
60+
>T : Symbol(T, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 19, 19))
61+
>NumericDictionary : Symbol(NumericDictionary, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 4, 1))
62+
>T : Symbol(T, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 19, 19))
63+
64+
callback: DictionaryIterator<T, TResult>
65+
>callback : Symbol(callback, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 20, 63))
66+
>DictionaryIterator : Symbol(DictionaryIterator, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 14, 13))
67+
>T : Symbol(T, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 19, 19))
68+
>TResult : Symbol(TResult, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 19, 21))
69+
70+
): Dictionary<TResult> {
71+
>Dictionary : Symbol(Dictionary, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 0, 0))
72+
>TResult : Symbol(TResult, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 19, 21))
73+
74+
return null as any;
75+
}
76+
// End types from Lodash.
77+
78+
interface Foo {
79+
>Foo : Symbol(Foo, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 24, 1))
80+
81+
foo: string;
82+
>foo : Symbol(Foo.foo, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 27, 15))
83+
}
84+
85+
interface Bar {
86+
>Bar : Symbol(Bar, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 29, 1))
87+
88+
bar: string;
89+
>bar : Symbol(Bar.bar, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 31, 15))
90+
}
91+
92+
export function fooToBar(
93+
>fooToBar : Symbol(fooToBar, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 33, 1))
94+
95+
foos: Record<string, Foo>
96+
>foos : Symbol(foos, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 35, 25))
97+
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))
98+
>Foo : Symbol(Foo, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 24, 1))
99+
100+
): Record<string, Bar | null> {
101+
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))
102+
>Bar : Symbol(Bar, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 29, 1))
103+
104+
const result = foos == null ? {} : mapValues(foos, f => f.foo);
105+
>result : Symbol(result, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 38, 7))
106+
>foos : Symbol(foos, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 35, 25))
107+
>mapValues : Symbol(mapValues, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 16, 77))
108+
>foos : Symbol(foos, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 35, 25))
109+
>f : Symbol(f, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 38, 52))
110+
>f.foo : Symbol(Foo.foo, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 27, 15))
111+
>f : Symbol(f, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 38, 52))
112+
>foo : Symbol(Foo.foo, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 27, 15))
113+
114+
// This line _should_ fail, because `result` is not the right type.
115+
return result;
116+
>result : Symbol(result, Decl(emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts, 38, 7))
117+
}
118+
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
=== tests/cases/compiler/emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts ===
2+
// This should behave the same as emptyObjectNotSubtypeOfIndexSignatureContainingObject2.ts
3+
// Begin types from Lodash.
4+
interface Dictionary<T> {
5+
[index: string]: T;
6+
>index : string
7+
}
8+
9+
interface NumericDictionary<T> {
10+
[index: number]: T;
11+
>index : number
12+
}
13+
14+
type ObjectIterator<TObject, TResult> = (
15+
>ObjectIterator : ObjectIterator<TObject, TResult>
16+
17+
value: TObject[keyof TObject],
18+
>value : TObject[keyof TObject]
19+
20+
key: string,
21+
>key : string
22+
23+
collection: TObject
24+
>collection : TObject
25+
26+
) => TResult;
27+
28+
type DictionaryIterator<T, TResult> = ObjectIterator<Dictionary<T>, TResult>;
29+
>DictionaryIterator : ObjectIterator<Dictionary<T>, TResult>
30+
31+
// In lodash.d.ts this function has many overloads, but this seems to be the problematic one.
32+
function mapValues<T, TResult>(
33+
>mapValues : <T, TResult>(obj: Dictionary<T> | NumericDictionary<T>, callback: ObjectIterator<Dictionary<T>, TResult>) => Dictionary<TResult>
34+
35+
obj: Dictionary<T> | NumericDictionary<T> | null | undefined,
36+
>obj : Dictionary<T> | NumericDictionary<T>
37+
>null : null
38+
39+
callback: DictionaryIterator<T, TResult>
40+
>callback : ObjectIterator<Dictionary<T>, TResult>
41+
42+
): Dictionary<TResult> {
43+
return null as any;
44+
>null as any : any
45+
>null : null
46+
}
47+
// End types from Lodash.
48+
49+
interface Foo {
50+
foo: string;
51+
>foo : string
52+
}
53+
54+
interface Bar {
55+
bar: string;
56+
>bar : string
57+
}
58+
59+
export function fooToBar(
60+
>fooToBar : (foos: Record<string, Foo>) => Record<string, Bar>
61+
62+
foos: Record<string, Foo>
63+
>foos : Record<string, Foo>
64+
65+
): Record<string, Bar | null> {
66+
>null : null
67+
68+
const result = foos == null ? {} : mapValues(foos, f => f.foo);
69+
>result : Dictionary<string>
70+
>foos == null ? {} : mapValues(foos, f => f.foo) : Dictionary<string>
71+
>foos == null : boolean
72+
>foos : Record<string, Foo>
73+
>null : null
74+
>{} : {}
75+
>mapValues(foos, f => f.foo) : Dictionary<string>
76+
>mapValues : <T, TResult>(obj: Dictionary<T> | NumericDictionary<T>, callback: ObjectIterator<Dictionary<T>, TResult>) => Dictionary<TResult>
77+
>foos : Record<string, Foo>
78+
>f => f.foo : (f: Foo) => string
79+
>f : Foo
80+
>f.foo : string
81+
>f : Foo
82+
>foo : string
83+
84+
// This line _should_ fail, because `result` is not the right type.
85+
return result;
86+
>result : Dictionary<string>
87+
}
88+
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
tests/cases/compiler/emptyObjectNotSubtypeOfIndexSignatureContainingObject2.ts(42,3): error TS2322: Type 'Dictionary<string>' is not assignable to type 'Record<string, Bar>'.
2+
Index signatures are incompatible.
3+
Type 'string' is not assignable to type 'Bar'.
4+
5+
6+
==== tests/cases/compiler/emptyObjectNotSubtypeOfIndexSignatureContainingObject2.ts (1 errors) ====
7+
// This should behave the same as emptyObjectNotSubtypeOfIndexSignatureContainingObject1.ts
8+
// Begin types from Lodash.
9+
interface Dictionary<T> {
10+
[index: string]: T;
11+
}
12+
13+
interface NumericDictionary<T> {
14+
[index: number]: T;
15+
}
16+
17+
type ObjectIterator<TObject, TResult> = (
18+
value: TObject[keyof TObject],
19+
key: string,
20+
collection: TObject
21+
) => TResult;
22+
23+
type DictionaryIterator<T, TResult> = ObjectIterator<Dictionary<T>, TResult>;
24+
25+
// In lodash.d.ts this function has many overloads, but this seems to be the problematic one.
26+
function mapValues<T, TResult>(
27+
obj: Dictionary<T> | NumericDictionary<T> | null | undefined,
28+
callback: DictionaryIterator<T, TResult>
29+
): Dictionary<TResult> {
30+
return null as any;
31+
}
32+
// End types from Lodash.
33+
34+
interface Foo {
35+
foo: string;
36+
}
37+
38+
interface Bar {
39+
bar: string;
40+
}
41+
42+
export function fooToBar(
43+
foos: Record<string, Foo>
44+
): Record<string, Bar | null> {
45+
const wat = mapValues(foos, f => f.foo);
46+
const result = foos == null ? {} : mapValues(foos, f => f.foo);
47+
// This line _should_ fail, because `result` is not the right type.
48+
return result;
49+
~~~~~~~~~~~~~~
50+
!!! error TS2322: Type 'Dictionary<string>' is not assignable to type 'Record<string, Bar>'.
51+
!!! error TS2322: Index signatures are incompatible.
52+
!!! error TS2322: Type 'string' is not assignable to type 'Bar'.
53+
}
54+

0 commit comments

Comments
 (0)