0
votes

I'm trying to build a table interface that provides strict typing on the value a table column has and a function that executes on the column value and returns a string. I'm having issues using generics in my function parameter however. Below is a simple example of what I'm doing. I'm using Typescript 3.9.5

/**
 * Interface for of a single column
 */
interface ColumnInfo<T, K extends keyof T> {
    column: K,
    cellFactory: (value: T[K]) => string
}

/**
 * Interface for all table columns. Only single row for debugging purposes.
 */
interface Columns<T> {
    columns: ColumnInfo<T, keyof T>;
};

/**
 * Row implementation
 */
type RowImpl = {
    id: string; 
    c1: string; //Could be number, Date, etc, simplified for now
    c2: string;
}

/**
 * id column implementation
 */
const columnInfo: ColumnInfo<RowImpl, 'id'> = {
    column: 'id',
    cellFactory: () => ''
}

/**
 * Table columns Implementation. Currently only accepting a single column for debugging
 */
const columns: Columns<RowImpl> = {
    columns: columnInfo
}

I'm currently getting an error on the columns object stating...

Type 'ColumnInfo<RowImpl, "id">' is not assignable to type 'ColumnInfo<RowImpl, "id" | "c1" | "c2">'. Type '"id" | "c1" | "c2"' is not assignable to type '"id"'. Type '"c1"' is not assignable to type '"id"'.ts(2322)

I'm not sure why it's trying to assign the union to the specific type as I'm using T[K]. I simplified the cellFactory object to just be T[K] to narrow down the issue, however it works now.

interface ColumnInfo<T, K extends keyof T> {
    column: K,
    cellFactory: T[K]
}


/**
 * Interface for all table columns. Only single row for debugging purposes
 */
interface Columns<T> {
    columns: ColumnInfo<T, keyof T>;
};

/**
 * Row implementation
 */
type RowImpl = {
    id: string;
    c1: number;
    c2: string;
}

/**
 * id column implementation
 */
const columnInfo: ColumnInfo<RowImpl, 'id'> = {
    column: 'id',
    cellFactory: ''
}

/**
 * Table columns Implementation. Currently only accepting a single column
 */
const columns: Columns<RowImpl> = {
    columns: columnInfo
}

With cellFactory as T[K] instead of (val: T[K]) => string it works. Why is this? I'd assume if it can infer the T[K] both examples should work but the function parameter isn't. Can someone explain what my issue is and what I need to change to get a working interface?

Thanks

1
cellFactory: keyof typeof T should work for you, since K shouldn't extend from keyof in the first place (in this use case). - DivisionByZero
Hmm why shouldn't K extend from it? I'm trying to use it as a specific column so I can implement the cellFactory to use the type that field is returning aka 'id''s cellFactory would take in a string, vs some other column that could be taking in a number. I'm going to look up what keyof typeof does to see if that works with what I'm trying to do. Thanks for the suggestion. - Exuro

1 Answers

0
votes

The problems start to arise when you try to define an object that can work for multiple different columns.

interface Columns<T> {
    columns: ColumnInfo<T, keyof T>;
};

The keyof T here is ALL keys of T, not just one specific key.

When you are trying to assign a more specific instance like columnInfo: ColumnInfo<RowImpl, 'id'> to the broader type ColumnInfo<RowImpl, keyof RowImpl>, the column property is fine because id is assignable to keyof RowImpl.

It's the callback that's the problem. The broader type ColumnInfo<RowImpl, keyof RowImpl> expects a callback that takes all possible values of the RowImpl object, but you are giving it a function that can only accept one specific type. So cellFactory: (value: RowImpl['id']) => string is not assignable to cellFactory: (value: RowImpl[keyof RowImpl]) => string.

If you were to broaden the ColumnInfo so that value was any value of the object, the errors go away. But I don't recommend this because we lose information about the specific column type.

interface ColumnInfo<T, K extends keyof T> {
    column: K,
    cellFactory: (value: T[keyof T]) => string
}

I have dealt with this particular problem myself, and my solution was to use a mapped object type where the keys of the columnInfo match the keys of RowImpl. This allows us to declare that the ColumnInfo for a given key must be for that column only. We do not need to widen to keyof T so we avoid the errors.

type Columns<T> = {
    [K in keyof T]?: ColumnInfo<T, K>;
};
const columns: Columns<RowImpl> = {
    id: columnInfo, // ok
    c1: columnInfo, // error: Type '"id"' is not assignable to type '"c1"'
}

Typescript Playground Link