As a followup to my previous post, I have the following function:
export const getUniqueValuesForFilters = <
RowType extends AnyObject,
Columns extends readonly { field: string }[],
>(
rows: RowType[],
columnConfig: readonly {
field: Columns[number]['field'];
valueGetter?: ({ row }: { row: RowType }) => string;
}[],
...filters: readonly { field: Columns[number]['field'] }[][]
): Record<Columns[number]['field'], FilterOption[]> => {
const fields = filters.map(list => list?.map(f => f.field)).flat();
const uniqueValuesTable = fields.reduce((obj, field) => {
const allValuesForField = rows.map(row => {
const column = columnConfig.find(c => c.field === field)
// we only need a valueGetter if we're not directly getting the value from that property of the row.
if (!column.valueGetter) return row[field as keyof RowType];
return column.valueGetter({ row });
});
const uniqueValuesForField = [...new Set(allValuesForField)];
const valuesAsOptions = uniqueValuesForField.map(value => ({
value,
label: value,
}));
return {
...obj,
[field]: valuesAsOptions,
};
}, {});
return uniqueValuesTable as Record<Columns[number]['field'], FilterOption[]>;
};
The issue I'm having is that typescript is saying that column
is possibly undefined. I understand that Array.prototype.find
could possibly fail, which would indeed leave column
undefined.
However, columnConfig
and filters
are properly typed such that all the filters' field
properties should be valid fields of the columnConfig
and therefore I would imagine that typescript would know that find
should always succeed.
Casting column
with as typeof columnConfig[number];
silences the error (as does a non-null assertion), but I don't think I should need that.
Hovering over both c.field
and field
in the callback of columnConfig.find
shows that they are both of the same type, Columns[number]["field"]
. I do also see that uniqueValuesTable
is being inferred as (string | RowType[keyof RowType])[]
-- which seems fine to me but I thought mentioning because my brain is pretty bursting here and maybe I'm missing something.
For what it's worth, the following test passes and has no TS errors.
describe('getUniqueValuesForFilters', () => {
it('works, god dammit', () => {
const columnConfig = [
{ field: 'name' },
{
field: 'uppercaseName',
valueGetter: ({ row: { name } }: { row: { name: string } }) => name.toUpperCase(),
},
] as const;
const rows = [{ name: 'alice' }, { name: 'bob' }, { name: 'Bob' }];
const filters: Array<Array<{ field: typeof columnConfig[number]['field'] }>> = [
[{ field: 'name' }],
[{ field: 'uppercaseName' }],
];
const result = getUniqueValuesForFilters(rows, columnConfig, ...filters);
expect(result['name'].map(o => o.value)).toEqual(expect.arrayContaining(['alice', 'bob', 'Bob']));
expect(result['uppercaseName'].map(o => o.value)).toEqual(expect.arrayContaining(['ALICE', 'BOB']));
});
});
CodePudding user response:
and therefore I would imagine that typescript would know that find should always succeed.
No, TypeScript will not go that far - that's just not the sort of inference TypeScript can make. .find
is explicitly typed to include undefined
as a possibility, no matter what.
find<S extends T>(
predicate: (this: void, value: T, index: number, obj: T[]) => value is S,
thisArg?: any
): S | undefined;
find(
predicate: (value: T, index: number, obj: T[]) => unknown,
thisArg?: any
): T | undefined;
The only type-related information passed to .find
(and thereby the only thing TypeScript can infer as a result from that) is
- the type of an array item, and
- (optionally) a generic argument that asserts that, if a value is found, the value is of a certain type
But there's nothing that will go through the logic of your program and identify that something absolutely will be found by .find
.
If you're absolutely sure it'll always find a match, just assert that the result isn't undefined
.
const column = columnConfig.find(c => c.field === field)!;