typescriptag-gridtypescript-genericsag-grid-angulartypescript-types

How can I use the field property of a ColDef to infer the types of other properties?


I am trying to define a type based on ColDef<TData> which infers the type of valueFormatter from the given value of field. For example, consider the following Order type:

type Order = {
    id: string,
    product: string,
    quantity: number,
    timestamp: Date,
}

When defining a table of Order rows in an Angular project, the column definitions will look something like:

const gridOptions: GridOptions<Order> = {
    // Define 3 columns
    columnDefs: [
        { field: 'id' },
        { field: 'product' },
        {
            field: 'quantity',
            // params has inferred type ValueFormatterParams<Order, any>
            // a stricter type would be ValueFormatterParams<Order, number> 
            valueFormatter: (params) => params.value.toLocaleString(),
        },
    ],
    // Other grid options...
};

My issue is that nothing stops me from adding the following column definition:

{
    field: 'timestamp',
    // Wrong type for params! The runtime type of params.value will be Date, not number!
    valueFormatter: (params: ValueFormatterParams<Order, number>) => `${params.value + 1}`,
}

I want the above code to raise a type error from the incorrect type parameters. Is there any way to tell TypeScript that the type of valueFormatter should be determined by the value of the given field property?

I want something to this effect:

type StrictColDef<TData> = ColDef<TData> & {
    field: TProperty extends keyof TData, // This gets inferred somehow
    valueFormatter?: (params: ValueFormatterParams<TData, TData[TProperty]>): string,
};

type StrictGridOptions<TData> = GridOptions<TData> & { columnDefs: StrictColDef<TData>[] };

const gridOptions: StrictGridOptions<Order> = {
    // Define 3 columns
    columnDefs: [
        { field: 'id' },
        { field: 'product' },
        {
            field: 'quantity',
            // params now has inferred type ValueFormatterParams<Order, number>, yay!
            valueFormatter: (params) => params.value.toLocaleString(),
        },
        {
            field: 'timestamp',
            // type error because the compiler has inferred type for params.value: Date
            valueFormatter: (params) => `${params.value + 1}`,
        },
    ],
    // Other grid options...
};

I've tried doing exactly this, but it just isn't how types are constructed in TypeScript.


Solution

  • I got a solution using type mapping! I wish there was a way to do this kind of type inference natively, but this is a pretty minimal workaround.

    Playground link

    type Order = {
        id: string,
        price: number,
    };
    
    type AllColDefsAsProperties<T> = {
        [Property in keyof T]: {
            field: Property,
            valueFormatter?: (value: T[Property]) => string,
        }
    };
    
    type Values<T> = T[keyof T];
    
    type ColDef<T> = Values<AllColDefsAsProperties<T>>;
    
    const columnDefs: ColDef<Order>[] = [
        {
            field: "id",
            // value has inferred type string
            valueFormatter: (value) => value,
        },
        {
            field: "price",
            valueFormatter: Intl.NumberFormat("en-US", { currency: "USD" }).format,
        },
        {
            field: "price",
            // value has inferred type number, invalid return type!
            //@ts-expect-error
            valueFormatter: (value) => value,
        },
        // Compiler expects an object whose formatter has number as argument
        //@ts-expect-error
        {
            field: "price",
            valueFormatter: (value: string) => value,
        },
    ];