Home > Enterprise >  Type-constrained "find" call is still possibly undefined
Type-constrained "find" call is still possibly undefined

Time:06-21

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)!;
  • Related