Descendants of 'Model' cannot be upcast
Reproduction
- Clone https://github.com/MathieuCouette/ember-data-issues
- Check out
model-upcasting-issue - Run
npm install && npm run lint
Description
Implicit upcasting of indirect descendants of Model does not work when the child has an attribute or relationship that the parent does not.
Source file: https://github.com/MathieuCouette/ember-data-issues/blob/d2b31433b1212b03879288990b763f9299c33e34/app/components/product.ts
Argument of type 'PaymentMethodCcModel' is not assignable to parameter of type 'PaymentMethodModel'.
Types of property 'eachAttribute' are incompatible.
Type '<T>(callback: (this: NoInfer<T> | undefined, key: "last4" | "obfuscatedIdentifier", meta: LegacyAttributeField) => void, binding?: T | undefined) => void' is not assignable to type '<T>(callback: (this: NoInfer<T> | undefined, key: never, meta: LegacyAttributeField) => void, binding?: T | undefined) => void'.
Types of parameters 'callback' and 'callback' are incompatible.
Types of parameters 'key' and 'key' are incompatible.
Type 'string' is not assignable to type 'never'.
Type 'string' is not assignable to type 'never'.
I am guessing that the cause is the same as #9405.
Versions
├── @babel/[email protected]
├── @ember-data-types/[email protected]
├── @ember-data-types/[email protected]
├── @ember-data-types/[email protected]
├── @ember-data-types/[email protected]
├── @ember-data-types/[email protected]
├── @ember-data-types/[email protected]
├── @ember-data-types/[email protected]
├── @ember-data-types/[email protected]
├── @ember-data-types/[email protected]
├── @ember-data-types/[email protected]
├── @ember/[email protected]
├── @ember/[email protected]
├── @ember/[email protected]
├── @glimmer/[email protected]
├── @glimmer/[email protected]
├── @glint/[email protected]
├── @glint/[email protected]
├── @tsconfig/[email protected]
├── @types/[email protected]
├── @types/[email protected]
├── @typescript-eslint/[email protected]
├── @typescript-eslint/[email protected]
├── @warp-drive-types/[email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
└── [email protected]
we have in our project the same issue (more than one usecase with different models)... is there any workaround how we can fix it without adding // @ts-expect error ...?
This is a generally unsolvable issue arising from the cyclical nature of inheritance in typescript. It's been discussed before in a few issues and there's really no solution we can do for it. It also only affects Model which is of course a concept we are working to phase out, so its also not generally worth the time to do anything particularly complicated for even the edge cases that could be improved.
That said, there's a few work arounds to this
- don't use Models as the types unless you also need the model APIs (this aligns better with the WarpDrive/SchemaRecord approach to types anyway)
- dont add methods directly to models (most of the time this is hit, folks are calling
this.belongsTo('bar')orthis.hasMany('foos'), or if you do need these APIs pass in the generic to them directly - dont use inheritance to implement polymorphism.
On note (3), polymorphism in WarpDrive/EmberData is entirely structural. In fact the original repo has a bug in that the relationship definitions on the subclasses do not properly report themselves as satisfying the abstract-type constraint (see usage of as below).
The example app models could have been implemented like so, to satisfy both the PaymentMethod interface and the polymorphism requirement.
import Model, { attr, belongsTo } from '@ember-data/model';
import type { Type } from '@warp-drive/core-types/symbols';
import type UserModel from './user';
export type PaymentMethod = {
[Type]: 'payment-method-cc' | 'payment-method-paypal';
user: UserModel;
}
class PaymentMethodPaypal extends Model implements PaymentMethod {
declare [Type]: 'payment-method-paypal';
@belongsTo<UserModel>('user', {
async: false,
inverse: 'paymentMethods',
as: 'payment-method'
})
declare user: UserModel;
@attr declare linkedEmail: string;
}
class PaymentMethodCc extends Model implements PaymentMethod {
declare [Type]: 'payment-method-cc';
@belongsTo<UserModel>('user', {
async: false,
inverse: 'paymentMethods',
as: 'payment-method'
})
declare user: UserModel;
@attr declare last4: string;
}