Skip to content

Commit 9477cbe

Browse files
committed
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
1 parent 297f12e commit 9477cbe

7 files changed

+288
-2
lines changed

Diff for: src/compiler/checker.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -7024,6 +7024,7 @@ namespace ts {
70247024

70257025
function createUnionOrIntersectionProperty(containingType: UnionOrIntersectionType, name: __String): Symbol | undefined {
70267026
let props: Symbol[] | undefined;
7027+
let indexTypes: Type[] | undefined;
70277028
const isUnion = containingType.flags & TypeFlags.Union;
70287029
const excludeModifiers = isUnion ? ModifierFlags.NonPublicAccessibilityModifier : 0;
70297030
// Flags we want to propagate to the result if they exist in all source symbols
@@ -7048,14 +7049,21 @@ namespace ts {
70487049
}
70497050
}
70507051
else if (isUnion) {
7051-
checkFlags |= CheckFlags.Partial;
7052+
const index = getIndexInfoOfType(type, IndexKind.String) || (isNumericLiteralName(name) && getIndexInfoOfType(type, IndexKind.Number));
7053+
if (index) {
7054+
checkFlags |= index.isReadonly ? CheckFlags.Readonly : 0;
7055+
indexTypes = append(indexTypes, index.type);
7056+
}
7057+
else {
7058+
checkFlags |= CheckFlags.Partial;
7059+
}
70527060
}
70537061
}
70547062
}
70557063
if (!props) {
70567064
return undefined;
70577065
}
7058-
if (props.length === 1 && !(checkFlags & CheckFlags.Partial)) {
7066+
if (props.length === 1 && !(checkFlags & CheckFlags.Partial) && !indexTypes) {
70597067
return props[0];
70607068
}
70617069
let declarations: Declaration[] | undefined;
@@ -7086,6 +7094,7 @@ namespace ts {
70867094
}
70877095
propTypes.push(type);
70887096
}
7097+
addRange(propTypes, indexTypes);
70897098
const result = createSymbol(SymbolFlags.Property | commonFlags, name, syntheticFlag | checkFlags);
70907099
result.containingType = containingType;
70917100
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,30 @@
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+
5+
6+
==== tests/cases/conformance/types/union/unionTypeWithIndexSignature.ts (2 errors) ====
7+
type Two = { foo: { bar: true }, baz: true } | { [s: string]: string };
8+
declare var u: Two;
9+
u.foo = 'bye'
10+
u.baz = 'hi'
11+
type Three = { foo: number } | { [s: string]: string } | { [s: string]: boolean };
12+
declare var v: Three;
13+
v.foo = false
14+
type Missing = { foo: number, bar: true } | { [s: string]: string } | { foo: boolean }
15+
declare var m: Missing;
16+
m.foo = 'hi'
17+
m.bar
18+
~~~
19+
!!! error TS2339: Property 'bar' does not exist on type 'Missing'.
20+
!!! error TS2339: Property 'bar' does not exist on type '{ [s: string]: string; }'.
21+
type RO = { foo: number } | { readonly [s: string]: string }
22+
declare var ro: RO;
23+
ro.foo = 'not allowed'
24+
~~~
25+
!!! error TS2540: Cannot assign to 'foo' because it is a constant or a read-only property.
26+
type Num = { '0': string } | { [n: number]: number }
27+
declare var num: Num;
28+
num[0] = 1
29+
num['0'] = 'ok'
30+
+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
21+
22+
//// [unionTypeWithIndexSignature.js]
23+
"use strict";
24+
u.foo = 'bye';
25+
u.baz = 'hi';
26+
v.foo = false;
27+
m.foo = 'hi';
28+
m.bar;
29+
ro.foo = 'not allowed';
30+
num[0] = 1;
31+
num['0'] = 'ok';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
=== tests/cases/conformance/types/union/unionTypeWithIndexSignature.ts ===
2+
type Two = { foo: { bar: true }, baz: true } | { [s: string]: string };
3+
>Two : Two
4+
>foo : { bar: true; }
5+
>bar : true
6+
>true : true
7+
>baz : true
8+
>true : true
9+
>s : string
10+
11+
declare var u: Two;
12+
>u : Two
13+
>Two : Two
14+
15+
u.foo = 'bye'
16+
>u.foo = 'bye' : "bye"
17+
>u.foo : string | { bar: true; }
18+
>u : Two
19+
>foo : string | { bar: true; }
20+
>'bye' : "bye"
21+
22+
u.baz = 'hi'
23+
>u.baz = 'hi' : "hi"
24+
>u.baz : string | true
25+
>u : Two
26+
>baz : string | true
27+
>'hi' : "hi"
28+
29+
type Three = { foo: number } | { [s: string]: string } | { [s: string]: boolean };
30+
>Three : Three
31+
>foo : number
32+
>s : string
33+
>s : string
34+
35+
declare var v: Three;
36+
>v : Three
37+
>Three : Three
38+
39+
v.foo = false
40+
>v.foo = false : false
41+
>v.foo : string | number | boolean
42+
>v : Three
43+
>foo : string | number | boolean
44+
>false : false
45+
46+
type Missing = { foo: number, bar: true } | { [s: string]: string } | { foo: boolean }
47+
>Missing : Missing
48+
>foo : number
49+
>bar : true
50+
>true : true
51+
>s : string
52+
>foo : boolean
53+
54+
declare var m: Missing;
55+
>m : Missing
56+
>Missing : Missing
57+
58+
m.foo = 'hi'
59+
>m.foo = 'hi' : "hi"
60+
>m.foo : string | number | boolean
61+
>m : Missing
62+
>foo : string | number | boolean
63+
>'hi' : "hi"
64+
65+
m.bar
66+
>m.bar : any
67+
>m : Missing
68+
>bar : any
69+
70+
type RO = { foo: number } | { readonly [s: string]: string }
71+
>RO : RO
72+
>foo : number
73+
>s : string
74+
75+
declare var ro: RO;
76+
>ro : RO
77+
>RO : RO
78+
79+
ro.foo = 'not allowed'
80+
>ro.foo = 'not allowed' : "not allowed"
81+
>ro.foo : any
82+
>ro : RO
83+
>foo : any
84+
>'not allowed' : "not allowed"
85+
86+
type Num = { '0': string } | { [n: number]: number }
87+
>Num : Num
88+
>'0' : string
89+
>n : number
90+
91+
declare var num: Num;
92+
>num : Num
93+
>Num : Num
94+
95+
num[0] = 1
96+
>num[0] = 1 : 1
97+
>num[0] : string | number
98+
>num : Num
99+
>0 : 0
100+
>1 : 1
101+
102+
num['0'] = 'ok'
103+
>num['0'] = 'ok' : "ok"
104+
>num['0'] : string | number
105+
>num : Num
106+
>'0' : "0"
107+
>'ok' : "ok"
108+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// @strict: true
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'

0 commit comments

Comments
 (0)