Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript - Property does not exist on extended generic interface

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>();
}
like image 299
TheMiddleMan Avatar asked Oct 26 '25 11:10

TheMiddleMan


1 Answers

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' });
like image 83
ug_ Avatar answered Oct 28 '25 01:10

ug_



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!