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

42 Views Asked by At

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.

1

There are 1 best solutions below

0
Ilan Segal On BEST ANSWER

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,
    },
];