Skip to content

Commit a5b1f95

Browse files
authored
Allow accessors to override non-class or abstract properties (#41994)
* remove too-late fix * Allow any property from a mapped type * turn off error for any non-class base * Also handle synthetic properties more laxly Originally from #42635, but this version is simpler. * update baselines * Update baselines * createUnionProperty of accessors creates an accessor Seems simple and doesn't break much. I need to double-check the few test failures, however. * Fix computation of write type of accessors * Calculate property-vs-accessor in existing loop Instead of looping over the props list 3 more times. * Undo synthetic accessor change * Minimise diff
1 parent 39f5dbf commit a5b1f95

File tree

9 files changed

+593
-4
lines changed

9 files changed

+593
-4
lines changed

src/compiler/checker.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7558,7 +7558,7 @@ namespace ts {
75587558
...!length(baseTypes) ? [] : [factory.createHeritageClause(SyntaxKind.ExtendsKeyword, map(baseTypes, b => serializeBaseType(b, staticBaseType, localName)))],
75597559
...!length(implementsExpressions) ? [] : [factory.createHeritageClause(SyntaxKind.ImplementsKeyword, implementsExpressions)]
75607560
];
7561-
const symbolProps = getNonInterhitedProperties(classType, baseTypes, getPropertiesOfType(classType));
7561+
const symbolProps = getNonInheritedProperties(classType, baseTypes, getPropertiesOfType(classType));
75627562
const publicSymbolProps = filter(symbolProps, s => {
75637563
// `valueDeclaration` could be undefined if inherited from
75647564
// a union/intersection base type, but inherited properties
@@ -40102,10 +40102,13 @@ namespace ts {
4010240102
const derivedPropertyFlags = derived.flags & SymbolFlags.PropertyOrAccessor;
4010340103
if (basePropertyFlags && derivedPropertyFlags) {
4010440104
// property/accessor is overridden with property/accessor
40105-
if (baseDeclarationFlags & ModifierFlags.Abstract && !(base.valueDeclaration && isPropertyDeclaration(base.valueDeclaration) && base.valueDeclaration.initializer)
40106-
|| base.valueDeclaration && base.valueDeclaration.parent.kind === SyntaxKind.InterfaceDeclaration
40105+
if ((getCheckFlags(base) & CheckFlags.Synthetic
40106+
? base.declarations?.some(d => isPropertyAbstractOrInterface(d, baseDeclarationFlags))
40107+
: base.declarations?.every(d => isPropertyAbstractOrInterface(d, baseDeclarationFlags)))
40108+
|| getCheckFlags(base) & CheckFlags.Mapped
4010740109
|| derived.valueDeclaration && isBinaryExpression(derived.valueDeclaration)) {
4010840110
// when the base property is abstract or from an interface, base/derived flags don't need to match
40111+
// for intersection properties, this must be true of *any* of the declarations, for others it must be true of *all*
4010940112
// same when the derived property is from an assignment
4011040113
continue;
4011140114
}
@@ -40163,7 +40166,12 @@ namespace ts {
4016340166
}
4016440167
}
4016540168

40166-
function getNonInterhitedProperties(type: InterfaceType, baseTypes: BaseType[], properties: Symbol[]) {
40169+
function isPropertyAbstractOrInterface(declaration: Declaration, baseDeclarationFlags: ModifierFlags) {
40170+
return baseDeclarationFlags & ModifierFlags.Abstract && (!isPropertyDeclaration(declaration) || !declaration.initializer)
40171+
|| isInterfaceDeclaration(declaration.parent);
40172+
}
40173+
40174+
function getNonInheritedProperties(type: InterfaceType, baseTypes: BaseType[], properties: Symbol[]) {
4016740175
if (!length(baseTypes)) {
4016840176
return properties;
4016940177
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//// [accessorsOverrideProperty8.ts]
2+
type Types = 'boolean' | 'unknown' | 'string';
3+
4+
type Properties<T extends { [key: string]: Types }> = {
5+
readonly [key in keyof T]: T[key] extends 'boolean' ? boolean : T[key] extends 'string' ? string : unknown
6+
}
7+
8+
type AnyCtor<P extends object> = new (...a: any[]) => P
9+
10+
declare function classWithProperties<T extends { [key: string]: Types }, P extends object>(properties: T, klass: AnyCtor<P>): {
11+
new(): P & Properties<T>;
12+
prototype: P & Properties<T>
13+
};
14+
15+
const Base = classWithProperties({
16+
get x() { return 'boolean' as const },
17+
y: 'string',
18+
}, class Base {
19+
});
20+
21+
class MyClass extends Base {
22+
get x() {
23+
return false;
24+
}
25+
get y() {
26+
return 'hi'
27+
}
28+
}
29+
30+
const mine = new MyClass();
31+
const value = mine.x;
32+
33+
34+
35+
//// [accessorsOverrideProperty8.js]
36+
const Base = classWithProperties({
37+
get x() { return 'boolean'; },
38+
y: 'string',
39+
}, class Base {
40+
});
41+
class MyClass extends Base {
42+
get x() {
43+
return false;
44+
}
45+
get y() {
46+
return 'hi';
47+
}
48+
}
49+
const mine = new MyClass();
50+
const value = mine.x;
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
=== tests/cases/conformance/classes/propertyMemberDeclarations/accessorsOverrideProperty8.ts ===
2+
type Types = 'boolean' | 'unknown' | 'string';
3+
>Types : Symbol(Types, Decl(accessorsOverrideProperty8.ts, 0, 0))
4+
5+
type Properties<T extends { [key: string]: Types }> = {
6+
>Properties : Symbol(Properties, Decl(accessorsOverrideProperty8.ts, 0, 46))
7+
>T : Symbol(T, Decl(accessorsOverrideProperty8.ts, 2, 16))
8+
>key : Symbol(key, Decl(accessorsOverrideProperty8.ts, 2, 29))
9+
>Types : Symbol(Types, Decl(accessorsOverrideProperty8.ts, 0, 0))
10+
11+
readonly [key in keyof T]: T[key] extends 'boolean' ? boolean : T[key] extends 'string' ? string : unknown
12+
>key : Symbol(key, Decl(accessorsOverrideProperty8.ts, 3, 14))
13+
>T : Symbol(T, Decl(accessorsOverrideProperty8.ts, 2, 16))
14+
>T : Symbol(T, Decl(accessorsOverrideProperty8.ts, 2, 16))
15+
>key : Symbol(key, Decl(accessorsOverrideProperty8.ts, 3, 14))
16+
>T : Symbol(T, Decl(accessorsOverrideProperty8.ts, 2, 16))
17+
>key : Symbol(key, Decl(accessorsOverrideProperty8.ts, 3, 14))
18+
}
19+
20+
type AnyCtor<P extends object> = new (...a: any[]) => P
21+
>AnyCtor : Symbol(AnyCtor, Decl(accessorsOverrideProperty8.ts, 4, 1))
22+
>P : Symbol(P, Decl(accessorsOverrideProperty8.ts, 6, 13))
23+
>a : Symbol(a, Decl(accessorsOverrideProperty8.ts, 6, 38))
24+
>P : Symbol(P, Decl(accessorsOverrideProperty8.ts, 6, 13))
25+
26+
declare function classWithProperties<T extends { [key: string]: Types }, P extends object>(properties: T, klass: AnyCtor<P>): {
27+
>classWithProperties : Symbol(classWithProperties, Decl(accessorsOverrideProperty8.ts, 6, 55))
28+
>T : Symbol(T, Decl(accessorsOverrideProperty8.ts, 8, 37))
29+
>key : Symbol(key, Decl(accessorsOverrideProperty8.ts, 8, 50))
30+
>Types : Symbol(Types, Decl(accessorsOverrideProperty8.ts, 0, 0))
31+
>P : Symbol(P, Decl(accessorsOverrideProperty8.ts, 8, 72))
32+
>properties : Symbol(properties, Decl(accessorsOverrideProperty8.ts, 8, 91))
33+
>T : Symbol(T, Decl(accessorsOverrideProperty8.ts, 8, 37))
34+
>klass : Symbol(klass, Decl(accessorsOverrideProperty8.ts, 8, 105))
35+
>AnyCtor : Symbol(AnyCtor, Decl(accessorsOverrideProperty8.ts, 4, 1))
36+
>P : Symbol(P, Decl(accessorsOverrideProperty8.ts, 8, 72))
37+
38+
new(): P & Properties<T>;
39+
>P : Symbol(P, Decl(accessorsOverrideProperty8.ts, 8, 72))
40+
>Properties : Symbol(Properties, Decl(accessorsOverrideProperty8.ts, 0, 46))
41+
>T : Symbol(T, Decl(accessorsOverrideProperty8.ts, 8, 37))
42+
43+
prototype: P & Properties<T>
44+
>prototype : Symbol(prototype, Decl(accessorsOverrideProperty8.ts, 9, 29))
45+
>P : Symbol(P, Decl(accessorsOverrideProperty8.ts, 8, 72))
46+
>Properties : Symbol(Properties, Decl(accessorsOverrideProperty8.ts, 0, 46))
47+
>T : Symbol(T, Decl(accessorsOverrideProperty8.ts, 8, 37))
48+
49+
};
50+
51+
const Base = classWithProperties({
52+
>Base : Symbol(Base, Decl(accessorsOverrideProperty8.ts, 13, 5))
53+
>classWithProperties : Symbol(classWithProperties, Decl(accessorsOverrideProperty8.ts, 6, 55))
54+
55+
get x() { return 'boolean' as const },
56+
>x : Symbol(x, Decl(accessorsOverrideProperty8.ts, 13, 34))
57+
>const : Symbol(const)
58+
59+
y: 'string',
60+
>y : Symbol(y, Decl(accessorsOverrideProperty8.ts, 14, 42))
61+
62+
}, class Base {
63+
>Base : Symbol(Base, Decl(accessorsOverrideProperty8.ts, 16, 2))
64+
65+
});
66+
67+
class MyClass extends Base {
68+
>MyClass : Symbol(MyClass, Decl(accessorsOverrideProperty8.ts, 17, 3))
69+
>Base : Symbol(Base, Decl(accessorsOverrideProperty8.ts, 13, 5))
70+
71+
get x() {
72+
>x : Symbol(MyClass.x, Decl(accessorsOverrideProperty8.ts, 19, 28))
73+
74+
return false;
75+
}
76+
get y() {
77+
>y : Symbol(MyClass.y, Decl(accessorsOverrideProperty8.ts, 22, 5))
78+
79+
return 'hi'
80+
}
81+
}
82+
83+
const mine = new MyClass();
84+
>mine : Symbol(mine, Decl(accessorsOverrideProperty8.ts, 28, 5))
85+
>MyClass : Symbol(MyClass, Decl(accessorsOverrideProperty8.ts, 17, 3))
86+
87+
const value = mine.x;
88+
>value : Symbol(value, Decl(accessorsOverrideProperty8.ts, 29, 5))
89+
>mine.x : Symbol(MyClass.x, Decl(accessorsOverrideProperty8.ts, 19, 28))
90+
>mine : Symbol(mine, Decl(accessorsOverrideProperty8.ts, 28, 5))
91+
>x : Symbol(MyClass.x, Decl(accessorsOverrideProperty8.ts, 19, 28))
92+
93+
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
=== tests/cases/conformance/classes/propertyMemberDeclarations/accessorsOverrideProperty8.ts ===
2+
type Types = 'boolean' | 'unknown' | 'string';
3+
>Types : "string" | "boolean" | "unknown"
4+
5+
type Properties<T extends { [key: string]: Types }> = {
6+
>Properties : Properties<T>
7+
>key : string
8+
9+
readonly [key in keyof T]: T[key] extends 'boolean' ? boolean : T[key] extends 'string' ? string : unknown
10+
}
11+
12+
type AnyCtor<P extends object> = new (...a: any[]) => P
13+
>AnyCtor : AnyCtor<P>
14+
>a : any[]
15+
16+
declare function classWithProperties<T extends { [key: string]: Types }, P extends object>(properties: T, klass: AnyCtor<P>): {
17+
>classWithProperties : <T extends { [key: string]: Types; }, P extends object>(properties: T, klass: AnyCtor<P>) => { new (): P & Properties<T>; prototype: P & Properties<T>;}
18+
>key : string
19+
>properties : T
20+
>klass : AnyCtor<P>
21+
22+
new(): P & Properties<T>;
23+
prototype: P & Properties<T>
24+
>prototype : P & Properties<T>
25+
26+
};
27+
28+
const Base = classWithProperties({
29+
>Base : { new (): Base & Properties<{ readonly x: "boolean"; y: "string"; }>; prototype: Base & Properties<{ readonly x: "boolean"; y: "string"; }>; }
30+
>classWithProperties({ get x() { return 'boolean' as const }, y: 'string',}, class Base {}) : { new (): Base & Properties<{ readonly x: "boolean"; y: "string"; }>; prototype: Base & Properties<{ readonly x: "boolean"; y: "string"; }>; }
31+
>classWithProperties : <T extends { [key: string]: Types; }, P extends object>(properties: T, klass: AnyCtor<P>) => { new (): P & Properties<T>; prototype: P & Properties<T>; }
32+
>{ get x() { return 'boolean' as const }, y: 'string',} : { readonly x: "boolean"; y: "string"; }
33+
34+
get x() { return 'boolean' as const },
35+
>x : "boolean"
36+
>'boolean' as const : "boolean"
37+
>'boolean' : "boolean"
38+
39+
y: 'string',
40+
>y : "string"
41+
>'string' : "string"
42+
43+
}, class Base {
44+
>class Base {} : typeof Base
45+
>Base : typeof Base
46+
47+
});
48+
49+
class MyClass extends Base {
50+
>MyClass : MyClass
51+
>Base : Base & Properties<{ readonly x: "boolean"; y: "string"; }>
52+
53+
get x() {
54+
>x : boolean
55+
56+
return false;
57+
>false : false
58+
}
59+
get y() {
60+
>y : string
61+
62+
return 'hi'
63+
>'hi' : "hi"
64+
}
65+
}
66+
67+
const mine = new MyClass();
68+
>mine : MyClass
69+
>new MyClass() : MyClass
70+
>MyClass : typeof MyClass
71+
72+
const value = mine.x;
73+
>value : boolean
74+
>mine.x : boolean
75+
>mine : MyClass
76+
>x : boolean
77+
78+
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
//// [accessorsOverrideProperty9.ts]
2+
// #41347, based on microsoft/rushstack
3+
4+
// Mixin utilities
5+
export type Constructor<T = {}> = new (...args: any[]) => T;
6+
export type PropertiesOf<T> = { [K in keyof T]: T[K] };
7+
8+
interface IApiItemConstructor extends Constructor<ApiItem>, PropertiesOf<typeof ApiItem> {}
9+
10+
// Base class
11+
class ApiItem {
12+
public get members(): ReadonlyArray<ApiItem> {
13+
return [];
14+
}
15+
}
16+
17+
// Normal subclass
18+
class ApiEnumMember extends ApiItem {
19+
}
20+
21+
// Mixin base class
22+
interface ApiItemContainerMixin extends ApiItem {
23+
readonly members: ReadonlyArray<ApiItem>;
24+
}
25+
26+
function ApiItemContainerMixin<TBaseClass extends IApiItemConstructor>(
27+
baseClass: TBaseClass
28+
): TBaseClass & (new (...args: any[]) => ApiItemContainerMixin) {
29+
abstract class MixedClass extends baseClass implements ApiItemContainerMixin {
30+
public constructor(...args: any[]) {
31+
super(...args);
32+
}
33+
34+
public get members(): ReadonlyArray<ApiItem> {
35+
return [];
36+
}
37+
}
38+
39+
return MixedClass;
40+
}
41+
42+
// Subclass inheriting from mixin
43+
export class ApiEnum extends ApiItemContainerMixin(ApiItem) {
44+
// This worked prior to TypeScript 4.0:
45+
public get members(): ReadonlyArray<ApiEnumMember> {
46+
return [];
47+
}
48+
}
49+
50+
51+
//// [accessorsOverrideProperty9.js]
52+
// #41347, based on microsoft/rushstack
53+
// Base class
54+
class ApiItem {
55+
get members() {
56+
return [];
57+
}
58+
}
59+
// Normal subclass
60+
class ApiEnumMember extends ApiItem {
61+
}
62+
function ApiItemContainerMixin(baseClass) {
63+
class MixedClass extends baseClass {
64+
constructor(...args) {
65+
super(...args);
66+
}
67+
get members() {
68+
return [];
69+
}
70+
}
71+
return MixedClass;
72+
}
73+
// Subclass inheriting from mixin
74+
export class ApiEnum extends ApiItemContainerMixin(ApiItem) {
75+
// This worked prior to TypeScript 4.0:
76+
get members() {
77+
return [];
78+
}
79+
}

0 commit comments

Comments
 (0)