Set-only accessors spread to undefined (#28213)

* Set-only accessors spread to undefined

Previously they were skipped. The runtime behaviour is to create a
property of type undefined, unlike (for example) spreading numbers or
other primitives. So now spreading a set-only accessor creates a
property of type undefined:

```ts
const o: { foo: undefined } = { ...{ set foo(v: number) { } } }
```

Notably, `o.foo: undefined` not `number`.

Fixes #26337

* Fix isSpreadableProperty oversimplification
This commit is contained in:
Nathan Shively-Sanders 2018-10-29 14:51:12 -07:00 committed by GitHub
parent 60efb65931
commit 64ff195426
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 71 additions and 17 deletions

View File

@ -4608,7 +4608,7 @@ namespace ts {
if (!names.has(prop.escapedName)
&& !(getDeclarationModifierFlagsFromSymbol(prop) & (ModifierFlags.Private | ModifierFlags.Protected))
&& isSpreadableProperty(prop)) {
members.set(prop.escapedName, getNonReadonlySymbol(prop));
members.set(prop.escapedName, getSpreadSymbol(prop));
}
}
const stringIndexInfo = getIndexInfoOfType(source, IndexKind.String);
@ -9887,7 +9887,7 @@ namespace ts {
skippedPrivateMembers.set(rightProp.escapedName, true);
}
else if (isSpreadableProperty(rightProp)) {
members.set(rightProp.escapedName, getNonReadonlySymbol(rightProp));
members.set(rightProp.escapedName, getSpreadSymbol(rightProp));
}
}
@ -9911,7 +9911,7 @@ namespace ts {
}
}
else {
members.set(leftProp.escapedName, getNonReadonlySymbol(leftProp));
members.set(leftProp.escapedName, getSpreadSymbol(leftProp));
}
}
@ -9929,18 +9929,19 @@ namespace ts {
/** We approximate own properties as non-methods plus methods that are inside the object literal */
function isSpreadableProperty(prop: Symbol): boolean {
return prop.flags & (SymbolFlags.Method | SymbolFlags.GetAccessor)
? !prop.declarations.some(decl => isClassLike(decl.parent))
: !(prop.flags & SymbolFlags.SetAccessor); // Setter without getter is not spreadable
return !(prop.flags & (SymbolFlags.Method | SymbolFlags.GetAccessor | SymbolFlags.SetAccessor)) ||
!prop.declarations.some(decl => isClassLike(decl.parent));
}
function getNonReadonlySymbol(prop: Symbol) {
if (!isReadonlySymbol(prop)) {
function getSpreadSymbol(prop: Symbol) {
const isReadonly = isReadonlySymbol(prop);
const isSetonlyAccessor = prop.flags & SymbolFlags.SetAccessor && !(prop.flags & SymbolFlags.GetAccessor);
if (!isReadonly && !isSetonlyAccessor) {
return prop;
}
const flags = SymbolFlags.Property | (prop.flags & SymbolFlags.Optional);
const result = createSymbol(flags, prop.escapedName);
result.type = getTypeOfSymbol(prop);
result.type = isSetonlyAccessor ? undefinedType : getTypeOfSymbol(prop);
result.declarations = prop.declarations;
result.nameType = prop.nameType;
result.syntheticOrigin = prop;

View File

@ -13,7 +13,7 @@ tests/cases/conformance/types/spread/objectSpreadNegative.ts(34,20): error TS269
tests/cases/conformance/types/spread/objectSpreadNegative.ts(36,20): error TS2698: Spread types may only be created from object types.
tests/cases/conformance/types/spread/objectSpreadNegative.ts(38,19): error TS2698: Spread types may only be created from object types.
tests/cases/conformance/types/spread/objectSpreadNegative.ts(43,1): error TS2349: Cannot invoke an expression whose type lacks a call signature. Type '{}' has no compatible call signatures.
tests/cases/conformance/types/spread/objectSpreadNegative.ts(47,12): error TS2339: Property 'b' does not exist on type '{}'.
tests/cases/conformance/types/spread/objectSpreadNegative.ts(47,1): error TS2322: Type '12' is not assignable to type 'undefined'.
tests/cases/conformance/types/spread/objectSpreadNegative.ts(53,9): error TS2339: Property 'm' does not exist on type '{ p: number; }'.
tests/cases/conformance/types/spread/objectSpreadNegative.ts(58,11): error TS2339: Property 'a' does not exist on type '{}'.
tests/cases/conformance/types/spread/objectSpreadNegative.ts(62,14): error TS2698: Spread types may only be created from object types.
@ -95,8 +95,8 @@ tests/cases/conformance/types/spread/objectSpreadNegative.ts(65,14): error TS269
// write-only properties get skipped
let setterOnly = { ...{ set b (bad: number) { } } };
setterOnly.b = 12; // error, 'b' does not exist
~
!!! error TS2339: Property 'b' does not exist on type '{}'.
~~~~~~~~~~~~
!!! error TS2322: Type '12' is not assignable to type 'undefined'.
// methods are skipped because they aren't enumerable
class C { p = 1; m() { } }

View File

@ -132,7 +132,9 @@ let setterOnly = { ...{ set b (bad: number) { } } };
>bad : Symbol(bad, Decl(objectSpreadNegative.ts, 45, 31))
setterOnly.b = 12; // error, 'b' does not exist
>setterOnly.b : Symbol(b, Decl(objectSpreadNegative.ts, 45, 23))
>setterOnly : Symbol(setterOnly, Decl(objectSpreadNegative.ts, 45, 3))
>b : Symbol(b, Decl(objectSpreadNegative.ts, 45, 23))
// methods are skipped because they aren't enumerable
class C { p = 1; m() { } }

View File

@ -173,17 +173,17 @@ spreadFunc(); // error, no call signature
// write-only properties get skipped
let setterOnly = { ...{ set b (bad: number) { } } };
>setterOnly : {}
>{ ...{ set b (bad: number) { } } } : {}
>setterOnly : { b: undefined; }
>{ ...{ set b (bad: number) { } } } : { b: undefined; }
>{ set b (bad: number) { } } : { b: number; }
>b : number
>bad : number
setterOnly.b = 12; // error, 'b' does not exist
>setterOnly.b = 12 : 12
>setterOnly.b : any
>setterOnly : {}
>b : any
>setterOnly.b : undefined
>setterOnly : { b: undefined; }
>b : undefined
>12 : 12
// methods are skipped because they aren't enumerable

View File

@ -0,0 +1,9 @@
//// [objectSpreadSetonlyAccessor.ts]
const o1: { foo: number, bar: undefined } = { foo: 1, ... { set bar(_v: number) { } } }
const o2: { foo: undefined } = { foo: 1, ... { set foo(_v: number) { } } }
//// [objectSpreadSetonlyAccessor.js]
"use strict";
const o1 = { foo: 1, ...{ set bar(_v) { } } };
const o2 = { foo: 1, ...{ set foo(_v) { } } };

View File

@ -0,0 +1,16 @@
=== tests/cases/conformance/types/spread/objectSpreadSetonlyAccessor.ts ===
const o1: { foo: number, bar: undefined } = { foo: 1, ... { set bar(_v: number) { } } }
>o1 : Symbol(o1, Decl(objectSpreadSetonlyAccessor.ts, 0, 5))
>foo : Symbol(foo, Decl(objectSpreadSetonlyAccessor.ts, 0, 11))
>bar : Symbol(bar, Decl(objectSpreadSetonlyAccessor.ts, 0, 24))
>foo : Symbol(foo, Decl(objectSpreadSetonlyAccessor.ts, 0, 45))
>bar : Symbol(bar, Decl(objectSpreadSetonlyAccessor.ts, 0, 59))
>_v : Symbol(_v, Decl(objectSpreadSetonlyAccessor.ts, 0, 68))
const o2: { foo: undefined } = { foo: 1, ... { set foo(_v: number) { } } }
>o2 : Symbol(o2, Decl(objectSpreadSetonlyAccessor.ts, 1, 5))
>foo : Symbol(foo, Decl(objectSpreadSetonlyAccessor.ts, 1, 11))
>foo : Symbol(foo, Decl(objectSpreadSetonlyAccessor.ts, 1, 32))
>foo : Symbol(foo, Decl(objectSpreadSetonlyAccessor.ts, 1, 46))
>_v : Symbol(_v, Decl(objectSpreadSetonlyAccessor.ts, 1, 55))

View File

@ -0,0 +1,22 @@
=== tests/cases/conformance/types/spread/objectSpreadSetonlyAccessor.ts ===
const o1: { foo: number, bar: undefined } = { foo: 1, ... { set bar(_v: number) { } } }
>o1 : { foo: number; bar: undefined; }
>foo : number
>bar : undefined
>{ foo: 1, ... { set bar(_v: number) { } } } : { bar: undefined; foo: number; }
>foo : number
>1 : 1
>{ set bar(_v: number) { } } : { bar: number; }
>bar : number
>_v : number
const o2: { foo: undefined } = { foo: 1, ... { set foo(_v: number) { } } }
>o2 : { foo: undefined; }
>foo : undefined
>{ foo: 1, ... { set foo(_v: number) { } } } : { foo: undefined; }
>foo : number
>1 : 1
>{ set foo(_v: number) { } } : { foo: number; }
>foo : number
>_v : number

View File

@ -0,0 +1,4 @@
// @strict: true
// @target: esnext
const o1: { foo: number, bar: undefined } = { foo: 1, ... { set bar(_v: number) { } } }
const o2: { foo: undefined } = { foo: 1, ... { set foo(_v: number) { } } }