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.
This commit is contained in:
Nathan Shively-Sanders 2018-07-06 10:46:05 -07:00 committed by GitHub
parent fd007e747d
commit c228924543
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 423 additions and 2 deletions

View file

@ -5904,6 +5904,12 @@ namespace ts {
&& isTypeUsableAsLateBoundName(checkComputedPropertyName(node));
}
function isLateBoundName(name: __String): boolean {
return (name as string).charCodeAt(0) === CharacterCodes._ &&
(name as string).charCodeAt(1) === CharacterCodes._ &&
(name as string).charCodeAt(2) === CharacterCodes.at;
}
/**
* Indicates whether a declaration has a late-bindable dynamic name.
*/
@ -7010,6 +7016,7 @@ namespace ts {
function createUnionOrIntersectionProperty(containingType: UnionOrIntersectionType, name: __String): Symbol | undefined {
let props: Symbol[] | undefined;
let indexTypes: Type[] | undefined;
const isUnion = containingType.flags & TypeFlags.Union;
const excludeModifiers = isUnion ? ModifierFlags.NonPublicAccessibilityModifier : 0;
// Flags we want to propagate to the result if they exist in all source symbols
@ -7034,14 +7041,21 @@ namespace ts {
}
}
else if (isUnion) {
checkFlags |= CheckFlags.Partial;
const index = !isLateBoundName(name) && ((isNumericLiteralName(name) && getIndexInfoOfType(type, IndexKind.Number)) || getIndexInfoOfType(type, IndexKind.String));
if (index) {
checkFlags |= index.isReadonly ? CheckFlags.Readonly : 0;
indexTypes = append(indexTypes, index.type);
}
else {
checkFlags |= CheckFlags.Partial;
}
}
}
}
if (!props) {
return undefined;
}
if (props.length === 1 && !(checkFlags & CheckFlags.Partial)) {
if (props.length === 1 && !(checkFlags & CheckFlags.Partial) && !indexTypes) {
return props[0];
}
let declarations: Declaration[] | undefined;
@ -7072,6 +7086,7 @@ namespace ts {
}
propTypes.push(type);
}
addRange(propTypes, indexTypes);
const result = createSymbol(SymbolFlags.Property | commonFlags, name, syntheticFlag | checkFlags);
result.containingType = containingType;
if (!hasNonUniformValueDeclaration && commonValueDeclaration) {

View file

@ -14,7 +14,9 @@ var bb;
var bbb = new mod.Baz();
>bbb : Symbol(bbb, Decl(use.js, 5, 3))
>mod.Baz : Symbol(Baz)
>mod : Symbol(mod, Decl(use.js, 0, 3))
>Baz : Symbol(Baz)
=== tests/cases/conformance/jsdoc/mod1.js ===
// error

View file

@ -0,0 +1,47 @@
tests/cases/conformance/types/union/unionTypeWithIndexSignature.ts(11,3): error TS2339: Property 'bar' does not exist on type 'Missing'.
Property 'bar' does not exist on type '{ [s: string]: string; }'.
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.
tests/cases/conformance/types/union/unionTypeWithIndexSignature.ts(24,1): error TS7017: Element implicitly has an 'any' type because type 'Both' has no index signature.
tests/cases/conformance/types/union/unionTypeWithIndexSignature.ts(25,1): error TS2322: Type '"not ok"' is not assignable to type 'number'.
tests/cases/conformance/types/union/unionTypeWithIndexSignature.ts(26,1): error TS7017: Element implicitly has an 'any' type because type 'Both' has no index signature.
==== tests/cases/conformance/types/union/unionTypeWithIndexSignature.ts (5 errors) ====
type Two = { foo: { bar: true }, baz: true } | { [s: string]: string };
declare var u: Two
u.foo = 'bye'
u.baz = 'hi'
type Three = { foo: number } | { [s: string]: string } | { [s: string]: boolean };
declare var v: Three
v.foo = false
type Missing = { foo: number, bar: true } | { [s: string]: string } | { foo: boolean }
declare var m: Missing
m.foo = 'hi'
m.bar
~~~
!!! error TS2339: Property 'bar' does not exist on type 'Missing'.
!!! error TS2339: Property 'bar' does not exist on type '{ [s: string]: string; }'.
type RO = { foo: number } | { readonly [s: string]: string }
declare var ro: RO
ro.foo = 'not allowed'
~~~
!!! error TS2540: Cannot assign to 'foo' because it is a constant or a read-only property.
type Num = { '0': string } | { [n: number]: number }
declare var num: Num
num[0] = 1
num['0'] = 'ok'
const sym = Symbol()
type Both = { s: number, '0': number, [sym]: boolean } | { [n: number]: number, [s: string]: string | number }
declare var both: Both
both['s'] = 'ok'
both[0] = 1
both[1] = 0 // not ok
~~~~~~~
!!! error TS7017: Element implicitly has an 'any' type because type 'Both' has no index signature.
both[0] = 'not ok'
~~~~~~~
!!! error TS2322: Type '"not ok"' is not assignable to type 'number'.
both[sym] = 'not ok'
~~~~~~~~~
!!! error TS7017: Element implicitly has an 'any' type because type 'Both' has no index signature.

View file

@ -0,0 +1,45 @@
//// [unionTypeWithIndexSignature.ts]
type Two = { foo: { bar: true }, baz: true } | { [s: string]: string };
declare var u: Two
u.foo = 'bye'
u.baz = 'hi'
type Three = { foo: number } | { [s: string]: string } | { [s: string]: boolean };
declare var v: Three
v.foo = false
type Missing = { foo: number, bar: true } | { [s: string]: string } | { foo: boolean }
declare var m: Missing
m.foo = 'hi'
m.bar
type RO = { foo: number } | { readonly [s: string]: string }
declare var ro: RO
ro.foo = 'not allowed'
type Num = { '0': string } | { [n: number]: number }
declare var num: Num
num[0] = 1
num['0'] = 'ok'
const sym = Symbol()
type Both = { s: number, '0': number, [sym]: boolean } | { [n: number]: number, [s: string]: string | number }
declare var both: Both
both['s'] = 'ok'
both[0] = 1
both[1] = 0 // not ok
both[0] = 'not ok'
both[sym] = 'not ok'
//// [unionTypeWithIndexSignature.js]
"use strict";
u.foo = 'bye';
u.baz = 'hi';
v.foo = false;
m.foo = 'hi';
m.bar;
ro.foo = 'not allowed';
num[0] = 1;
num['0'] = 'ok';
const sym = Symbol();
both['s'] = 'ok';
both[0] = 1;
both[1] = 0; // not ok
both[0] = 'not ok';
both[sym] = 'not ok';

View file

@ -0,0 +1,123 @@
=== tests/cases/conformance/types/union/unionTypeWithIndexSignature.ts ===
type Two = { foo: { bar: true }, baz: true } | { [s: string]: string };
>Two : Symbol(Two, Decl(unionTypeWithIndexSignature.ts, 0, 0))
>foo : Symbol(foo, Decl(unionTypeWithIndexSignature.ts, 0, 12))
>bar : Symbol(bar, Decl(unionTypeWithIndexSignature.ts, 0, 19))
>baz : Symbol(baz, Decl(unionTypeWithIndexSignature.ts, 0, 32))
>s : Symbol(s, Decl(unionTypeWithIndexSignature.ts, 0, 50))
declare var u: Two
>u : Symbol(u, Decl(unionTypeWithIndexSignature.ts, 1, 11))
>Two : Symbol(Two, Decl(unionTypeWithIndexSignature.ts, 0, 0))
u.foo = 'bye'
>u.foo : Symbol(foo, Decl(unionTypeWithIndexSignature.ts, 0, 12))
>u : Symbol(u, Decl(unionTypeWithIndexSignature.ts, 1, 11))
>foo : Symbol(foo, Decl(unionTypeWithIndexSignature.ts, 0, 12))
u.baz = 'hi'
>u.baz : Symbol(baz, Decl(unionTypeWithIndexSignature.ts, 0, 32))
>u : Symbol(u, Decl(unionTypeWithIndexSignature.ts, 1, 11))
>baz : Symbol(baz, Decl(unionTypeWithIndexSignature.ts, 0, 32))
type Three = { foo: number } | { [s: string]: string } | { [s: string]: boolean };
>Three : Symbol(Three, Decl(unionTypeWithIndexSignature.ts, 3, 12))
>foo : Symbol(foo, Decl(unionTypeWithIndexSignature.ts, 4, 14))
>s : Symbol(s, Decl(unionTypeWithIndexSignature.ts, 4, 34))
>s : Symbol(s, Decl(unionTypeWithIndexSignature.ts, 4, 60))
declare var v: Three
>v : Symbol(v, Decl(unionTypeWithIndexSignature.ts, 5, 11))
>Three : Symbol(Three, Decl(unionTypeWithIndexSignature.ts, 3, 12))
v.foo = false
>v.foo : Symbol(foo, Decl(unionTypeWithIndexSignature.ts, 4, 14))
>v : Symbol(v, Decl(unionTypeWithIndexSignature.ts, 5, 11))
>foo : Symbol(foo, Decl(unionTypeWithIndexSignature.ts, 4, 14))
type Missing = { foo: number, bar: true } | { [s: string]: string } | { foo: boolean }
>Missing : Symbol(Missing, Decl(unionTypeWithIndexSignature.ts, 6, 13))
>foo : Symbol(foo, Decl(unionTypeWithIndexSignature.ts, 7, 16))
>bar : Symbol(bar, Decl(unionTypeWithIndexSignature.ts, 7, 29))
>s : Symbol(s, Decl(unionTypeWithIndexSignature.ts, 7, 47))
>foo : Symbol(foo, Decl(unionTypeWithIndexSignature.ts, 7, 71))
declare var m: Missing
>m : Symbol(m, Decl(unionTypeWithIndexSignature.ts, 8, 11))
>Missing : Symbol(Missing, Decl(unionTypeWithIndexSignature.ts, 6, 13))
m.foo = 'hi'
>m.foo : Symbol(foo, Decl(unionTypeWithIndexSignature.ts, 7, 16), Decl(unionTypeWithIndexSignature.ts, 7, 71))
>m : Symbol(m, Decl(unionTypeWithIndexSignature.ts, 8, 11))
>foo : Symbol(foo, Decl(unionTypeWithIndexSignature.ts, 7, 16), Decl(unionTypeWithIndexSignature.ts, 7, 71))
m.bar
>m : Symbol(m, Decl(unionTypeWithIndexSignature.ts, 8, 11))
type RO = { foo: number } | { readonly [s: string]: string }
>RO : Symbol(RO, Decl(unionTypeWithIndexSignature.ts, 10, 5))
>foo : Symbol(foo, Decl(unionTypeWithIndexSignature.ts, 11, 11))
>s : Symbol(s, Decl(unionTypeWithIndexSignature.ts, 11, 40))
declare var ro: RO
>ro : Symbol(ro, Decl(unionTypeWithIndexSignature.ts, 12, 11))
>RO : Symbol(RO, Decl(unionTypeWithIndexSignature.ts, 10, 5))
ro.foo = 'not allowed'
>ro.foo : Symbol(foo, Decl(unionTypeWithIndexSignature.ts, 11, 11))
>ro : Symbol(ro, Decl(unionTypeWithIndexSignature.ts, 12, 11))
>foo : Symbol(foo, Decl(unionTypeWithIndexSignature.ts, 11, 11))
type Num = { '0': string } | { [n: number]: number }
>Num : Symbol(Num, Decl(unionTypeWithIndexSignature.ts, 13, 22))
>'0' : Symbol('0', Decl(unionTypeWithIndexSignature.ts, 14, 12))
>n : Symbol(n, Decl(unionTypeWithIndexSignature.ts, 14, 32))
declare var num: Num
>num : Symbol(num, Decl(unionTypeWithIndexSignature.ts, 15, 11))
>Num : Symbol(Num, Decl(unionTypeWithIndexSignature.ts, 13, 22))
num[0] = 1
>num : Symbol(num, Decl(unionTypeWithIndexSignature.ts, 15, 11))
>0 : Symbol('0', Decl(unionTypeWithIndexSignature.ts, 14, 12))
num['0'] = 'ok'
>num : Symbol(num, Decl(unionTypeWithIndexSignature.ts, 15, 11))
>'0' : Symbol('0', Decl(unionTypeWithIndexSignature.ts, 14, 12))
const sym = Symbol()
>sym : Symbol(sym, Decl(unionTypeWithIndexSignature.ts, 18, 5))
>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, --, --))
type Both = { s: number, '0': number, [sym]: boolean } | { [n: number]: number, [s: string]: string | number }
>Both : Symbol(Both, Decl(unionTypeWithIndexSignature.ts, 18, 20))
>s : Symbol(s, Decl(unionTypeWithIndexSignature.ts, 19, 13))
>'0' : Symbol('0', Decl(unionTypeWithIndexSignature.ts, 19, 24))
>[sym] : Symbol([sym], Decl(unionTypeWithIndexSignature.ts, 19, 37))
>sym : Symbol(sym, Decl(unionTypeWithIndexSignature.ts, 18, 5))
>n : Symbol(n, Decl(unionTypeWithIndexSignature.ts, 19, 60))
>s : Symbol(s, Decl(unionTypeWithIndexSignature.ts, 19, 81))
declare var both: Both
>both : Symbol(both, Decl(unionTypeWithIndexSignature.ts, 20, 11))
>Both : Symbol(Both, Decl(unionTypeWithIndexSignature.ts, 18, 20))
both['s'] = 'ok'
>both : Symbol(both, Decl(unionTypeWithIndexSignature.ts, 20, 11))
>'s' : Symbol(s, Decl(unionTypeWithIndexSignature.ts, 19, 13))
both[0] = 1
>both : Symbol(both, Decl(unionTypeWithIndexSignature.ts, 20, 11))
>0 : Symbol('0', Decl(unionTypeWithIndexSignature.ts, 19, 24))
both[1] = 0 // not ok
>both : Symbol(both, Decl(unionTypeWithIndexSignature.ts, 20, 11))
both[0] = 'not ok'
>both : Symbol(both, Decl(unionTypeWithIndexSignature.ts, 20, 11))
>0 : Symbol('0', Decl(unionTypeWithIndexSignature.ts, 19, 24))
both[sym] = 'not ok'
>both : Symbol(both, Decl(unionTypeWithIndexSignature.ts, 20, 11))
>sym : Symbol(sym, Decl(unionTypeWithIndexSignature.ts, 18, 5))

View file

@ -0,0 +1,161 @@
=== tests/cases/conformance/types/union/unionTypeWithIndexSignature.ts ===
type Two = { foo: { bar: true }, baz: true } | { [s: string]: string };
>Two : Two
>foo : { bar: true; }
>bar : true
>true : true
>baz : true
>true : true
>s : string
declare var u: Two
>u : Two
>Two : Two
u.foo = 'bye'
>u.foo = 'bye' : "bye"
>u.foo : string | { bar: true; }
>u : Two
>foo : string | { bar: true; }
>'bye' : "bye"
u.baz = 'hi'
>u.baz = 'hi' : "hi"
>u.baz : string | true
>u : Two
>baz : string | true
>'hi' : "hi"
type Three = { foo: number } | { [s: string]: string } | { [s: string]: boolean };
>Three : Three
>foo : number
>s : string
>s : string
declare var v: Three
>v : Three
>Three : Three
v.foo = false
>v.foo = false : false
>v.foo : string | number | boolean
>v : Three
>foo : string | number | boolean
>false : false
type Missing = { foo: number, bar: true } | { [s: string]: string } | { foo: boolean }
>Missing : Missing
>foo : number
>bar : true
>true : true
>s : string
>foo : boolean
declare var m: Missing
>m : Missing
>Missing : Missing
m.foo = 'hi'
>m.foo = 'hi' : "hi"
>m.foo : string | number | boolean
>m : Missing
>foo : string | number | boolean
>'hi' : "hi"
m.bar
>m.bar : any
>m : Missing
>bar : any
type RO = { foo: number } | { readonly [s: string]: string }
>RO : RO
>foo : number
>s : string
declare var ro: RO
>ro : RO
>RO : RO
ro.foo = 'not allowed'
>ro.foo = 'not allowed' : "not allowed"
>ro.foo : any
>ro : RO
>foo : any
>'not allowed' : "not allowed"
type Num = { '0': string } | { [n: number]: number }
>Num : Num
>'0' : string
>n : number
declare var num: Num
>num : Num
>Num : Num
num[0] = 1
>num[0] = 1 : 1
>num[0] : string | number
>num : Num
>0 : 0
>1 : 1
num['0'] = 'ok'
>num['0'] = 'ok' : "ok"
>num['0'] : string | number
>num : Num
>'0' : "0"
>'ok' : "ok"
const sym = Symbol()
>sym : unique symbol
>Symbol() : unique symbol
>Symbol : SymbolConstructor
type Both = { s: number, '0': number, [sym]: boolean } | { [n: number]: number, [s: string]: string | number }
>Both : Both
>s : number
>'0' : number
>[sym] : boolean
>sym : unique symbol
>n : number
>s : string
declare var both: Both
>both : Both
>Both : Both
both['s'] = 'ok'
>both['s'] = 'ok' : "ok"
>both['s'] : string | number
>both : Both
>'s' : "s"
>'ok' : "ok"
both[0] = 1
>both[0] = 1 : 1
>both[0] : number
>both : Both
>0 : 0
>1 : 1
both[1] = 0 // not ok
>both[1] = 0 : 0
>both[1] : any
>both : Both
>1 : 1
>0 : 0
both[0] = 'not ok'
>both[0] = 'not ok' : "not ok"
>both[0] : number
>both : Both
>0 : 0
>'not ok' : "not ok"
both[sym] = 'not ok'
>both[sym] = 'not ok' : "not ok"
>both[sym] : any
>both : Both
>sym : unique symbol
>'not ok' : "not ok"

View file

@ -0,0 +1,28 @@
// @target: esnext
// @strict: true
type Two = { foo: { bar: true }, baz: true } | { [s: string]: string };
declare var u: Two
u.foo = 'bye'
u.baz = 'hi'
type Three = { foo: number } | { [s: string]: string } | { [s: string]: boolean };
declare var v: Three
v.foo = false
type Missing = { foo: number, bar: true } | { [s: string]: string } | { foo: boolean }
declare var m: Missing
m.foo = 'hi'
m.bar
type RO = { foo: number } | { readonly [s: string]: string }
declare var ro: RO
ro.foo = 'not allowed'
type Num = { '0': string } | { [n: number]: number }
declare var num: Num
num[0] = 1
num['0'] = 'ok'
const sym = Symbol()
type Both = { s: number, '0': number, [sym]: boolean } | { [n: number]: number, [s: string]: string | number }
declare var both: Both
both['s'] = 'ok'
both[0] = 1
both[1] = 0 // not ok
both[0] = 'not ok'
both[sym] = 'not ok'