Search code examples
typescript

How should I fix this case of "'T' could be instantiated with a different subtype of constraint - ts(2345)"?


I've read multiple posts about this error, but can't figure out the right solution in my case.

I need to generate dummy rows throughout my web app, so I can display them as the loading state in MUI Skeleton.

I want to pass an object to this function with the required properties of a given type and return an array with IDs added.

export const generateDummyRows = <T extends { id: string }>(
  count: number,
  record: Omit<T, "id">,
): T[] => Array.from({ length: count }, (_, i) => ({ ...record, id: `${i}` }));

The Array.from expression gets this type error:

Type '(Omit<T, "id"> & { id: string; })[]' is not assignable to type 'T[]'.
  Type 'Omit<T, "id"> & { id: string; }' is not assignable to type 'T'.
    'Omit<T, "id"> & { id: string; }' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '{ id: string; }'.(2322)

Any ideas for best way to fix?

Example usage:

const dummyRows = generateDummyRows<Student>(10, {
  firstName: string,
  lastName: string,
  createdAt: Date.now(),
});

UPDATE for @jcalz's comment: I initially had record as optional because I have a record type in which all properties are optional, except for id. So it would allow for a call like generateDummyRows<Foo>(3) and get [{id: '0'}, {id: '1'}, {id: '2'}].

I'm open for other suggestions, e.g. don't have to use Omit, but would like to avoid using type assertion (as), because it seems like that overrides the issue, as in @Alexis' answer below. Correct?

Minimal reproducible example


Solution

  • The error you're getting is warning you about a real problem; it's not true in general that Omit<T, "id"> & {id: string} is the same as T, even if T is constrained to {id: string}. That's because the id property of T can be narrower than string, and then generateDummyRows() is almost certainly going to return something that violates the call signature. There are string literal types and unions of such types to worry about:

    interface Foo {
        id: "x" | "y"
        bar: string;
    }
    
    const dummyRows = generateDummyRows<Foo>(10, { bar: "abc" });
    //    ^? const dummyRows: Foo[]
    

    Here a Foo must have an id of either "x" or "y". It can't be "0" or "1" or anything else. According to the call signature of generateDummyRows() though, the function accepts a record of type {bar: string} and returns an array of Foo:

    dummyRows.forEach(v => ({ x: 0, y: 1 })[v.id].toFixed())
    

    But it really isn't an array of Foo[] because the id property is just wrong. So while the previous line compiles, you'll get an error at runtime, since ({ x: 0, y: 1 })[v.id] is undefined.

    See Why can't I return a generic 'T' to satisfy a Partial<T>? for more about this pitfall with trying to return specific values for generic types.


    My suggestion here is not to try to claim that the input is Omit<T, "id"> and the output is an array of T. Instead, let's just use T to mean the input (what you were calling Omit<T, id>, and then the output is an array of T & {id: string}. This is closer to being true (although it can still be false if T already contains an id property), and TypeScript sees object literal spread as producing intersections:

    const generateDummyRows = <T extends object>(
        count: number,
        record: T,
    ): (T & { id: string })[] =>
        Array.from({ length: count }, (_, i) => ({ ...record, id: `${i}` }));
    

    That compiles without error now. I annotated the return type to make it clear what's happening, but if you leave it out the call signature is the same. Now the problem from before can't happen, because TypeScript won't pretend that generateDummyRows acting on {bar: string} produces a Foo:

    const oops: Foo[] = generateDummyRows(10, { bar: "abc" }); // error!
    

    And you can write your original code like this:

    const dummyRows: Student[] = generateDummyRows(10, {
        firstName: 'John',
        lastName: 'Doe',
        createdAt: Date.now(),
    });
    

    Note that since I changed the meaning of T, you wouldn't call generateDummyRows<Student>(⋯). If you wanted, you could call generateDummyRows<Omit<Student, "id">>(⋯). Either way, the output is assignable to Student[]. It's easier to just not manually specify the generic type argument, and instead, annotate the dummyRows variable. If there's a problem, you'll see it there.

    Playground link to code