I wanted to write a function that sums columns from rows given as key-value arrays in TypeScript. Because I often know the TS-typed column names, I would like the function to detect typos or the usage of columns that have other than number
values.
Sometimes, however, I am using dynamic column names given by generic strings instead of string-literals. In that case, the function needs to check whether the column exists and has number
data at run-time.
I managed to write two versions, one correctly accepting the names of columns containing number
data and rejecting others when given as literals, and one correctly accepting and run-time checking column names given dynamically as generic strings. But of course, I would love to have one to work in both cases and still highlight when I put in a string-literal that does not exists as a column name or does not have number
data.
Any hints appreciated much!
Please see Playground Link for the relevant TS errors:
type KeyValue = { [key: string]: unknown };
type NumberFields<Row extends KeyValue> = {
[Property in keyof Row as Row[Property] extends number ? Property : never]: number;
};
type NumberFieldKey<Row extends KeyValue> = keyof NumberFields<Row>;
function columnSumLiteral<Row extends KeyValue>(
array: (Row & NumberFields<Row>)[],
field: NumberFieldKey<Row>
) {
return array.reduce<number>((acc, x) => {
return acc + x[field];
}, 0);
}
function columnSumGeneric<Row extends KeyValue>(
array: Row[],
field: string
) {
return array.reduce<number>((acc, x) => {
const value = x[field];
if (typeof value !== 'number') return acc;
return acc + value;
}, 0);
}
type Row = {
textCol: string;
numberCol: number;
};
const rows : Row[] = [
{
textCol: 'five',
numberCol: 5,
},
{
textCol: 'seven',
numberCol: 7,
},
{
textCol: 'nine',
numberCol: 9,
},
];
const numberColGeneric = 'numberCol' as string;
const textColGeneric = 'textCol' as string;
console.log('Can I join the two variants of `columnSum()` into an overloaded function and obtain the desired behavior?')
console.log('accepted as intended:', columnSumLiteral(rows, 'numberCol'));
console.log('rejected as intended, producing "undefined behavior" which is OK:', columnSumLiteral(rows, 'textCol'));
console.log('rejected but should be accepted and run-time checked:', columnSumLiteral(rows, numberColGeneric));
console.log('rejected but should be accepted and run-time checked:', columnSumLiteral(rows, textColGeneric));
console.log('accepted as intended:', columnSumGeneric(rows, 'numberCol'));
console.log('accepted but should be rejected:', columnSumGeneric(rows, 'textCol'));
console.log('accepted as intended and runtime checked:', columnSumGeneric(rows, numberColGeneric));
console.log('accepted as intended and runtime checked:', columnSumGeneric(rows, textColGeneric));