Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

typescript add missing fields to a type that should have them

Tags:

typescript

I'm trying to create a helper function for my database code. Every table in the database has some common fields (pk, creation date, etc) that is implemented as a common interface which the other table's interfaces extend. I want to make a function that automatically adds those common properties to an object and returns the new object, but typescript complains about subtype assignment in a way I can't understand...

Type '{ id: string; } & Pick<T, Exclude<keyof T, "id">>' is not assignable to type 'T'.
  '{ id: string; } & Pick<T, Exclude<keyof T, "id">>' is assignable to the constraint of type 
'T', but 'T' could be instantiated with a different subtype of constraint 'Common'

I've made a minimal code example on the TS playground to reproduce it. For a given type, T which extends the Common base interface, I want to take in an object containing all the required fields of T, except those that are in Common, and then return the object with the added fields.

interface Common {
    id: string;
}

interface Table extends Common {
    data: string;
}

function makeDbEntry<T extends Common>(initial: Omit<T, 'id'>): T {
    const common: Common = {
        id: 'random string',
    };
    const ret: T = {
        ...common,
        ...initial,
    };
    return ret;
}

Playground Link

There doesn't seem to be any way to remove the fields from T without explicitly listing their fields (can't do 'T minus Common'), as far as I can tell...

like image 573
zacaj Avatar asked Oct 28 '25 20:10

zacaj


1 Answers

Conditional types can't be fully resolved if they contain type parameters. This means that, at least for this use case, Omit<T, 'id'> is basically opaque, TS can't look inside what this type is, it is just a type which is distinct from T.

When you do the spread operation, TS correctly understands that this will generate an intersection between common and Omit<T, 'id'> ({ id: string; } & Omit<T, 'id'>). But since Omit is as I have said opaque, no further simplification of this type can be done. Even though to the human this is trivially resolvable to T, typescript does not know how to simplify this type.

If you replace T with a concrete type, then ts can easily understand that for example { id: string; } & Omit<Table, 'id'> is the same as Table, so this works:

function makeDbEntryForTable(initial: Omit<Table, 'id'>): Table {
    const common: Common = {
        id: 'random string',
    };
    const ret: Table = {
        ...common,
        ...initial,
    };
    return ret;
}

For the generic version, your best bet, as in most cases where the dev has a better understanding than the compiler, is to use a type assertion:

function makeDbEntry<T extends Common>(initial: Omit<T, 'id'>): T {
    const common: Common = {
        id: 'random string',
    };
    const ret: T = {
        ...common,
        ...initial,
    } as T;
    return ret;
}

Playground Link

like image 102
Titian Cernicova-Dragomir Avatar answered Oct 30 '25 15:10

Titian Cernicova-Dragomir