Elmer
Elmer

Reputation: 9437

Typescript generics, Type 'X' is not assignable to type 'Y'

I have the following piece of code:

interface DummyTableRow {
    id: number;
    name: string;
}
interface RowChange<Row extends object> {
    newRow: Row | null;
    oldRow: Row | null;
}
interface RowChangeBag {
    Dummy: RowChangeList<DummyTableRow>;
}
type RowChangeList<Row extends object> = Array<RowChange<Row>>;

function add<
    Row extends object,
    T extends keyof RowChangeBag,
    // error on next line: Type 'DummyTableRow' is not assignable to type 'Row'.
    L extends RowChangeList<Row> = RowChangeBag[T]
    >(
    bag: RowChangeBag,
    tableName: T,
    newRow: Row | null,
    oldRow: Row | null,
) {
    bag[tableName].push({ newRow, oldRow });
}

Why does it complain? Why is DummyTableRow not assignable to Row?

Or could this be a 'bug' in typescript? (i use version 2.6.2 for this example)

Upvotes: 1

Views: 1308

Answers (2)

Romain Deneau
Romain Deneau

Reputation: 3061

Your code is mixing two types with similar names but their are not compatibles:

  • DummyTableRow is your abstraction of the row → ok
  • The generic type constraint Row extends object: object is not compatible with DummyTableRow: id and name properties are missing. → Just do Row extends DummyTableRow.

Other issues:

  • RowChangeBag has to be generic too.
  • RowChangeBag is meant to store several RowChangeList, cf. T extends keyof RowChangeBag and bag[tableName] is a RowChangeList. In the code below, I've split it into 2 interfaces, so that it will be easier to add tables in it.
  • The L generic type constraint in the add method is not used.

My advices to enhance the code readibility:

  • Prefer generic type names starting with T: RowTRow
  • Then, DummyTableRow name can be simplified: just Row
  • Avoid using null; use only undefined.
  • Then, define property as optional rather than nullable.
  • Use T[] rather than Array<T> because it's shorter and more JavaScript idiomatic.

Fix proposal (tested on the TypeScript Playground):

interface Row {
    id: number;
    name: string;
}

interface RowChange<TRow extends Row> {
    newRow?: TRow;
    oldRow?: TRow;
}

type RowChangeList<TRow extends Row> = RowChange<TRow>[];

interface TableGroup<T> {
    Table1: T;
    Table2: T;
}

interface RowChangeBag<TRow extends Row>
    extends TableGroup<RowChangeList<TRow>> { }

function add<
    TRow extends Row,
    TKey extends keyof RowChangeBag<TRow>
>(
    bag: RowChangeBag<TRow>,
    tableName: TKey,
    newRow: TRow,
    oldRow: TRow,
) {
    bag[tableName].push({ newRow, oldRow });
}

Upvotes: 1

unional
unional

Reputation: 15619

This is not a TypeScript bug.

The behavior you saw can be reduced to the following:

interface DummyTableRow {
    id: number;
    name: string;
}

function doing<Row>() {
    let a: DummyTableRow
    let b: Row
    // error on next line: Type 'DummyTableRow' is not assignable to type 'Row'.
    b = a
}

The issue here is that since Row is a generic type, there is no way the compiler can tell whether a can be assigned to b, because b can be any type defined at usage, e.g.

doing<{ a: string }>()

Upvotes: 2

Related Questions