Scenario: I am attempting to create a base method I can reuse (works/doesNotWork) for a few different calls I need to make. The base method should do some setup as I do not want to repeat several times. The problem is, the generic I am passing to my setup call is telling me it expects a different type. All the properties I need exist on the base generic type, but I get typescript issues.
Error: Argument of type '{ SyncCount: number; }' is not assignable to parameter of type 'DeepPartial<T>'
Goal: I would like a solution using generics which would allow me to have a base method I can call for setup. Ideally, doesNotWork would be modified to work properly. Aos, please see the desired code (goal) in the example below and in the fiddle.
Here is a fiddle of my issue
type DeepPartial<T> = T extends object ? {
[P in keyof T]?: DeepPartial<T[P]>;
} : T;
interface IDbRecord {
readonly _id: string;
readonly _rev: string;
readonly DocumentType: string;
}
interface IBuilder<TEntity extends IDbRecord> {
defaults(value: DeepPartial<TEntity>): Builder<TEntity>
}
class Builder<TEntity extends IDbRecord> implements IBuilder<TEntity> {
defaults(value: DeepPartial<TEntity>): IBuilder<TEntity> {
return this;
}
}
interface IBaseEntity extends IDbRecord {
SyncCount: number;
SyncStatus: string
}
interface ICar extends IBaseEntity {
Model: string;
}
interface IPerson extends IBaseEntity {
Name: string
}
class Test {
protected builder<TEntity extends IDbRecord>() {
return new Builder<TEntity>();
}
private works<T extends IBaseEntity>() {
// not using the generic type works
return this.builder<IBaseEntity>().defaults({ SyncCount: 0 })
}
private doesNotWork<T extends IBaseEntity>() {
// using the generic type does not work here
return this.builder<T>().defaults({ SyncCount: 0 }) // ERROR
}
private someWorkingImplmentation<T extends IBaseEntity>() {
return this.builder<T>().defaults({ SyncCount: 0 });
}
// I want to avoid duplicate code below from defaults.
// A base setup method would be best
people = this.builder<IPerson>().defaults({ SyncCount: 0 });
cars = this.builder<ICar>().defaults({ SyncCount: 0 });
// GOAL
_people = this.someWorkingImplmentation<IPerson>();
_cars = this.someWorkingImplmentation<ICar>();
}
The type restriction in doesNotWork<T extends IBaseEntity> doesn't seem to propagate down to the method defaults. This seems to cause Typescript to think that the generic T could be anything, despite it being constrained to IBaseEntity. See this SO question for more detail.
Right now the constraints flow as such
doesNotWork<ICar> // return type inferred from builder<T>
builder<T> - this is where the constraints of IBaseEntity fall off
defaults<T>()
One simple solution is to invert the constraint by fixing your return type of the function:
private doesNotWork<T extends IBaseEntity>(): IBuilder<T> {
return this.builder<IBaseEntity>().defaults({ SyncCount: 0 })
}
By adding the return type of IBuilder<T> we are removing the requirement of generic T flowing through to the builder to determine our return type.
With this the following code now properly recognizes the fields of the interface:
_people = this.works<IPerson>().defaults({ Name: 'Foo' });
_cars = this.works<ICar>().defaults({ Model: 'jetta' });
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With