Skip to content

Commit c228924

Browse files
authored
Index signatures contribute properties to unions (#25307)
* Index signatures contribute properties to unions This means that in a union like this: ```ts type T = { foo: number } | { [s: string]: string } ``` `foo` is now a property of `T` with type `number | string`. Previously it was not. Two points of interest: 1. A readonly index signature makes the resulting union property readonly. 2. A numeric index signature only contributes number-named properties. Fixes #21141 * Correctly handle numeric and symbol property names 1. Symbol-named properties don't contribute to unions. 2. Number-named properties should use the numeric index signature type, if present, and fall back to the string index signature type, not the other way round.
1 parent fd007e7 commit c228924

7 files changed

+423
-2
lines changed

Diff for: src/compiler/checker.ts

+17-2
Original file line numberDiff line numberDiff line change
@@ -5904,6 +5904,12 @@ namespace ts {
59045904
&& isTypeUsableAsLateBoundName(checkComputedPropertyName(node));
59055905
}
59065906

5907+
function isLateBoundName(name: __String): boolean {
5908+
return (name as string).charCodeAt(0) === CharacterCodes._ &&
5909+
(name as string).charCodeAt(1) === CharacterCodes._ &&
5910+
(name as string).charCodeAt(2) === CharacterCodes.at;
5911+
}
5912+
59075913
/**
59085914
* Indicates whether a declaration has a late-bindable dynamic name.
59095915
*/
@@ -7010,6 +7016,7 @@ namespace ts {
70107016

70117017
function createUnionOrIntersectionProperty(containingType: UnionOrIntersectionType, name: __String): Symbol | undefined {
70127018
let props: Symbol[] | undefined;
7019+
let indexTypes: Type[] | undefined;
70137020
const isUnion = containingType.flags & TypeFlags.Union;
70147021
const excludeModifiers = isUnion ? ModifierFlags.NonPublicAccessibilityModifier : 0;
70157022
// Flags we want to propagate to the result if they exist in all source symbols
@@ -7034,14 +7041,21 @@ namespace ts {
70347041
}
70357042
}
70367043
else if (isUnion) {
7037-
checkFlags |= CheckFlags.Partial;
7044+
const index = !isLateBoundName(name) && ((isNumericLiteralName(name) && getIndexInfoOfType(type, IndexKind.Number)) || getIndexInfoOfType(type, IndexKind.String));
7045+
if (index) {
7046+
checkFlags |= index.isReadonly ? CheckFlags.Readonly : 0;
7047+
indexTypes = append(indexTypes, index.type);
7048+
}
7049+
else {
7050+
checkFlags |= CheckFlags.Partial;
7051+
}
70387052
}
70397053
}
70407054
}
70417055
if (!props) {
70427056
return undefined;
70437057
}
7044-
if (props.length === 1 && !(checkFlags & CheckFlags.Partial)) {
7058+
if (props.length === 1 && !(checkFlags & CheckFlags.Partial) && !indexTypes) {
70457059
return props[0];
70467060
}
70477061
let declarations: Declaration[] | undefined;
@@ -7072,6 +7086,7 @@ namespace ts {
70727086
}
70737087
propTypes.push(type);
70747088
}
7089+
addRange(propTypes, indexTypes);
70757090
const result = createSymbol(SymbolFlags.Property | commonFlags, name, syntheticFlag | checkFlags);
70767091
result.containingType = containingType;
70777092
if (!hasNonUniformValueDeclaration && commonValueDeclaration) {

Diff for: tests/baselines/reference/typedefCrossModule2.symbols

+2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ var bb;
1414

1515
var bbb = new mod.Baz();
1616
>bbb : Symbol(bbb, Decl(use.js, 5, 3))
17+
>mod.Baz : Symbol(Baz)
1718
>mod : Symbol(mod, Decl(use.js, 0, 3))
19+
>Baz : Symbol(Baz)
1820

1921
=== tests/cases/conformance/jsdoc/mod1.js ===
2022
// error
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
tests/cases/conformance/types/union/unionTypeWithIndexSignature.ts(11,3): error TS2339: Property 'bar' does not exist on type 'Missing'.
2+
Property 'bar' does not exist on type '{ [s: string]: string; }'.
3+
tests/cases/conformance/types/union/unionTypeWithIndexSignature.ts(14,4): error TS2540: Cannot assign to 'foo' because it is a constant or a read-only property.
4+
tests/cases/conformance/types/union/unionTypeWithIndexSignature.ts(24,1): error TS7017: Element implicitly has an 'any' type because type 'Both' has no index signature.
5+
tests/cases/conformance/types/union/unionTypeWithIndexSignature.ts(25,1): error TS2322: Type '"not ok"' is not assignable to type 'number'.
6+
tests/cases/conformance/types/union/unionTypeWithIndexSignature.ts(26,1): error TS7017: Element implicitly has an 'any' type because type 'Both' has no index signature.
7+
8+
9+
==== tests/cases/conformance/types/union/unionTypeWithIndexSignature.ts (5 errors) ====
10+
type Two = { foo: { bar: true }, baz: true } | { [s: string]: string };
11+
declare var u: Two
12+
u.foo = 'bye'
13+
u.baz = 'hi'
14+
type Three = { foo: number } | { [s: string]: string } | { [s: string]: boolean };
15+
declare var v: Three
16+
v.foo = false
17+
type Missing = { foo: number, bar: true } | { [s: string]: string } | { foo: boolean }
18+
declare var m: Missing
19+
m.foo = 'hi'
20+
m.bar
21+
~~~
22+
!!! error TS2339: Property 'bar' does not exist on type 'Missing'.
23+
!!! error TS2339: Property 'bar' does not exist on type '{ [s: string]: string; }'.
24+
type RO = { foo: number } | { readonly [s: string]: string }
25+
declare var ro: RO
26+
ro.foo = 'not allowed'
27+
~~~
28+
!!! error TS2540: Cannot assign to 'foo' because it is a constant or a read-only property.
29+
type Num = { '0': string } | { [n: number]: number }
30+
declare var num: Num
31+
num[0] = 1
32+
num['0'] = 'ok'
33+
const sym = Symbol()
34+
type Both = { s: number, '0': number, [sym]: boolean } | { [n: number]: number, [s: string]: string | number }
35+
declare var both: Both
36+
both['s'] = 'ok'
37+
both[0] = 1
38+
both[1] = 0 // not ok
39+
~~~~~~~
40+
!!! error TS7017: Element implicitly has an 'any' type because type 'Both' has no index signature.
41+
both[0] = 'not ok'
42+
~~~~~~~
43+
!!! error TS2322: Type '"not ok"' is not assignable to type 'number'.
44+
both[sym] = 'not ok'
45+
~~~~~~~~~
46+
!!! error TS7017: Element implicitly has an 'any' type because type 'Both' has no index signature.
47+
+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//// [unionTypeWithIndexSignature.ts]
2+
type Two = { foo: { bar: true }, baz: true } | { [s: string]: string };
3+
declare var u: Two
4+
u.foo = 'bye'
5+
u.baz = 'hi'
6+
type Three = { foo: number } | { [s: string]: string } | { [s: string]: boolean };
7+
declare var v: Three
8+
v.foo = false
9+
type Missing = { foo: number, bar: true } | { [s: string]: string } | { foo: boolean }
10+
declare var m: Missing
11+
m.foo = 'hi'
12+
m.bar
13+
type RO = { foo: number } | { readonly [s: string]: string }
14+
declare var ro: RO
15+
ro.foo = 'not allowed'
16+
type Num = { '0': string } | { [n: number]: number }
17+
declare var num: Num
18+
num[0] = 1
19+
num['0'] = 'ok'
20+
const sym = Symbol()
21+
type Both = { s: number, '0': number, [sym]: boolean } | { [n: number]: number, [s: string]: string | number }
22+
declare var both: Both
23+
both['s'] = 'ok'
24+
both[0] = 1
25+
both[1] = 0 // not ok
26+
both[0] = 'not ok'
27+
both[sym] = 'not ok'
28+
29+
30+
//// [unionTypeWithIndexSignature.js]
31+
"use strict";
32+
u.foo = 'bye';
33+
u.baz = 'hi';
34+
v.foo = false;
35+
m.foo = 'hi';
36+
m.bar;
37+
ro.foo = 'not allowed';
38+
num[0] = 1;
39+
num['0'] = 'ok';
40+
const sym = Symbol();
41+
both['s'] = 'ok';
42+
both[0] = 1;
43+
both[1] = 0; // not ok
44+
both[0] = 'not ok';
45+
both[sym] = 'not ok';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
=== tests/cases/conformance/types/union/unionTypeWithIndexSignature.ts ===
2+
type Two = { foo: { bar: true }, baz: true } | { [s: string]: string };
3+
>Two : Symbol(Two, Decl(unionTypeWithIndexSignature.ts, 0, 0))
4+
>foo : Symbol(foo, Decl(unionTypeWithIndexSignature.ts, 0, 12))
5+
>bar : Symbol(bar, Decl(unionTypeWithIndexSignature.ts, 0, 19))
6+
>baz : Symbol(baz, Decl(unionTypeWithIndexSignature.ts, 0, 32))
7+
>s : Symbol(s, Decl(unionTypeWithIndexSignature.ts, 0, 50))
8+
9+
declare var u: Two
10+
>u : Symbol(u, Decl(unionTypeWithIndexSignature.ts, 1, 11))
11+
>Two : Symbol(Two, Decl(unionTypeWithIndexSignature.ts, 0, 0))
12+
13+
u.foo = 'bye'
14+
>u.foo : Symbol(foo, Decl(unionTypeWithIndexSignature.ts, 0, 12))
15+
>u : Symbol(u, Decl(unionTypeWithIndexSignature.ts, 1, 11))
16+
>foo : Symbol(foo, Decl(unionTypeWithIndexSignature.ts, 0, 12))
17+
18+
u.baz = 'hi'
19+
>u.baz : Symbol(baz, Decl(unionTypeWithIndexSignature.ts, 0, 32))
20+
>u : Symbol(u, Decl(unionTypeWithIndexSignature.ts, 1, 11))
21+
>baz : Symbol(baz, Decl(unionTypeWithIndexSignature.ts, 0, 32))
22+
23+
type Three = { foo: number } | { [s: string]: string } | { [s: string]: boolean };
24+
>Three : Symbol(Three, Decl(unionTypeWithIndexSignature.ts, 3, 12))
25+
>foo : Symbol(foo, Decl(unionTypeWithIndexSignature.ts, 4, 14))
26+
>s : Symbol(s, Decl(unionTypeWithIndexSignature.ts, 4, 34))
27+
>s : Symbol(s, Decl(unionTypeWithIndexSignature.ts, 4, 60))
28+
29+
declare var v: Three
30+
>v : Symbol(v, Decl(unionTypeWithIndexSignature.ts, 5, 11))
31+
>Three : Symbol(Three, Decl(unionTypeWithIndexSignature.ts, 3, 12))
32+
33+
v.foo = false
34+
>v.foo : Symbol(foo, Decl(unionTypeWithIndexSignature.ts, 4, 14))
35+
>v : Symbol(v, Decl(unionTypeWithIndexSignature.ts, 5, 11))
36+
>foo : Symbol(foo, Decl(unionTypeWithIndexSignature.ts, 4, 14))
37+
38+
type Missing = { foo: number, bar: true } | { [s: string]: string } | { foo: boolean }
39+
>Missing : Symbol(Missing, Decl(unionTypeWithIndexSignature.ts, 6, 13))
40+
>foo : Symbol(foo, Decl(unionTypeWithIndexSignature.ts, 7, 16))
41+
>bar : Symbol(bar, Decl(unionTypeWithIndexSignature.ts, 7, 29))
42+
>s : Symbol(s, Decl(unionTypeWithIndexSignature.ts, 7, 47))
43+
>foo : Symbol(foo, Decl(unionTypeWithIndexSignature.ts, 7, 71))
44+
45+
declare var m: Missing
46+
>m : Symbol(m, Decl(unionTypeWithIndexSignature.ts, 8, 11))
47+
>Missing : Symbol(Missing, Decl(unionTypeWithIndexSignature.ts, 6, 13))
48+
49+
m.foo = 'hi'
50+
>m.foo : Symbol(foo, Decl(unionTypeWithIndexSignature.ts, 7, 16), Decl(unionTypeWithIndexSignature.ts, 7, 71))
51+
>m : Symbol(m, Decl(unionTypeWithIndexSignature.ts, 8, 11))
52+
>foo : Symbol(foo, Decl(unionTypeWithIndexSignature.ts, 7, 16), Decl(unionTypeWithIndexSignature.ts, 7, 71))
53+
54+
m.bar
55+
>m : Symbol(m, Decl(unionTypeWithIndexSignature.ts, 8, 11))
56+
57+
type RO = { foo: number } | { readonly [s: string]: string }
58+
>RO : Symbol(RO, Decl(unionTypeWithIndexSignature.ts, 10, 5))
59+
>foo : Symbol(foo, Decl(unionTypeWithIndexSignature.ts, 11, 11))
60+
>s : Symbol(s, Decl(unionTypeWithIndexSignature.ts, 11, 40))
61+
62+
declare var ro: RO
63+
>ro : Symbol(ro, Decl(unionTypeWithIndexSignature.ts, 12, 11))
64+
>RO : Symbol(RO, Decl(unionTypeWithIndexSignature.ts, 10, 5))
65+
66+
ro.foo = 'not allowed'
67+
>ro.foo : Symbol(foo, Decl(unionTypeWithIndexSignature.ts, 11, 11))
68+
>ro : Symbol(ro, Decl(unionTypeWithIndexSignature.ts, 12, 11))
69+
>foo : Symbol(foo, Decl(unionTypeWithIndexSignature.ts, 11, 11))
70+
71+
type Num = { '0': string } | { [n: number]: number }
72+
>Num : Symbol(Num, Decl(unionTypeWithIndexSignature.ts, 13, 22))
73+
>'0' : Symbol('0', Decl(unionTypeWithIndexSignature.ts, 14, 12))
74+
>n : Symbol(n, Decl(unionTypeWithIndexSignature.ts, 14, 32))
75+
76+
declare var num: Num
77+
>num : Symbol(num, Decl(unionTypeWithIndexSignature.ts, 15, 11))
78+
>Num : Symbol(Num, Decl(unionTypeWithIndexSignature.ts, 13, 22))
79+
80+
num[0] = 1
81+
>num : Symbol(num, Decl(unionTypeWithIndexSignature.ts, 15, 11))
82+
>0 : Symbol('0', Decl(unionTypeWithIndexSignature.ts, 14, 12))
83+
84+
num['0'] = 'ok'
85+
>num : Symbol(num, Decl(unionTypeWithIndexSignature.ts, 15, 11))
86+
>'0' : Symbol('0', Decl(unionTypeWithIndexSignature.ts, 14, 12))
87+
88+
const sym = Symbol()
89+
>sym : Symbol(sym, Decl(unionTypeWithIndexSignature.ts, 18, 5))
90+
>Symbol : Symbol(Symbol, Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.symbol.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --), Decl(lib.esnext.symbol.d.ts, --, --))
91+
92+
type Both = { s: number, '0': number, [sym]: boolean } | { [n: number]: number, [s: string]: string | number }
93+
>Both : Symbol(Both, Decl(unionTypeWithIndexSignature.ts, 18, 20))
94+
>s : Symbol(s, Decl(unionTypeWithIndexSignature.ts, 19, 13))
95+
>'0' : Symbol('0', Decl(unionTypeWithIndexSignature.ts, 19, 24))
96+
>[sym] : Symbol([sym], Decl(unionTypeWithIndexSignature.ts, 19, 37))
97+
>sym : Symbol(sym, Decl(unionTypeWithIndexSignature.ts, 18, 5))
98+
>n : Symbol(n, Decl(unionTypeWithIndexSignature.ts, 19, 60))
99+
>s : Symbol(s, Decl(unionTypeWithIndexSignature.ts, 19, 81))
100+
101+
declare var both: Both
102+
>both : Symbol(both, Decl(unionTypeWithIndexSignature.ts, 20, 11))
103+
>Both : Symbol(Both, Decl(unionTypeWithIndexSignature.ts, 18, 20))
104+
105+
both['s'] = 'ok'
106+
>both : Symbol(both, Decl(unionTypeWithIndexSignature.ts, 20, 11))
107+
>'s' : Symbol(s, Decl(unionTypeWithIndexSignature.ts, 19, 13))
108+
109+
both[0] = 1
110+
>both : Symbol(both, Decl(unionTypeWithIndexSignature.ts, 20, 11))
111+
>0 : Symbol('0', Decl(unionTypeWithIndexSignature.ts, 19, 24))
112+
113+
both[1] = 0 // not ok
114+
>both : Symbol(both, Decl(unionTypeWithIndexSignature.ts, 20, 11))
115+
116+
both[0] = 'not ok'
117+
>both : Symbol(both, Decl(unionTypeWithIndexSignature.ts, 20, 11))
118+
>0 : Symbol('0', Decl(unionTypeWithIndexSignature.ts, 19, 24))
119+
120+
both[sym] = 'not ok'
121+
>both : Symbol(both, Decl(unionTypeWithIndexSignature.ts, 20, 11))
122+
>sym : Symbol(sym, Decl(unionTypeWithIndexSignature.ts, 18, 5))
123+

0 commit comments

Comments
 (0)