4

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>();
}
Agrejus
  • 722
  • 7
  • 18
  • Are you looking for an explanation of why or a solution? I have solutions but don't have a concrete description of why and documentation to point to as my source. – ug_ Aug 10 '22 at 16:34
  • I can solve it by `return this.builder().defaults({ SyncCount: 0 } as DeepPartial)`. – tom10271 Aug 10 '22 at 16:40
  • Updated the goal @ug_. I would like a solution :) – Agrejus Aug 10 '22 at 16:47
  • what kind of solution? because the solution could be to not use the generic, i dont know what you want :/ – kelsny Aug 10 '22 at 17:06
  • Updated the goal @kelly, I would like to use generics in the solution – Agrejus Aug 10 '22 at 17:09

1 Answers1

2

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' });
ug_
  • 11,267
  • 2
  • 35
  • 52
  • 1
    Using your example I am able to get solution to work. My real world solution is a bit more complex, the fiddle is meant to be a simpler explanation of the issue. Nonetheless, I am able to get my complex solution to work. It is a bit odd why the constraints fall off. The properties exist on type `T`, but aren't accessible. Anyways, this is what I needed! Thank you!!! – Agrejus Aug 10 '22 at 17:52