1

TypeScript Playground Link

I am setting up some types with generics with work alongside @tanstack/table, this is my current code.

import { type ColumnFilter, type OnChangeFn, type TableState } from '@tanstack/react-table';

export interface DataTableColumnFiltersState<TData> extends ColumnFilter {
  id: Extract<keyof TData, string>;
  value: string;
}

type FilterState = DataTableColumnFiltersState<Record<'union' | 'of' | 'string', string>>;

type SetColumnFilters = OnChangeFn<TableState['columnFilters']>;

const setColumnFilters: OnChangeFn<FilterState[]> = () => {};

interface Props {
  setColumnFilters: SetColumnFilters;
}

const props: Props = {
  setColumnFilters
}

I am getting a type error on

const props: Props = {
  setColumnFilters
}

The full type error is

Type 'OnChangeFn<FilterState[]>' is not assignable to type 'SetColumnFilters'.
  Types of parameters 'updaterOrValue' and 'updaterOrValue' are incompatible.
    Type 'Updater<ColumnFiltersState>' is not assignable to type 'Updater<FilterState[]>'.
      Type 'ColumnFiltersState' is not assignable to type 'Updater<FilterState[]>'.
        Type 'ColumnFilter[]' is not assignable to type 'FilterState[]'.
          Type 'ColumnFilter' is not assignable to type 'FilterState'.
            Types of property 'id' are incompatible.
              Type 'string' is not assignable to type '"string" | "union" | "of"'.

I am just confused by the last line of the error Type 'string' is not assignable to type '"string" | "union" | "of"'.

Doesn't '"string" | "union" | "of"' imply a string type?

Is there any solution to this other than doing

const props: Props = {
  setColumnFilters: setColumnFilters as SetColumnFilters
};

Any help would be greatly appreciated.

Please see playground link below.

TypeScript Playground Link

mcclosa
  • 943
  • 7
  • 29
  • 59
  • 2
    "Doesn't `"✂"` imply a `string` type?" Yes, but not the other way around, which is what it needs to be. Otherwise you can get a runtime error when `Props` blithely accepts any string `id` in its `setColumnFilters` and the implementation can't handle arbitrary strings. You probably want `Props` to be generic. Both the problem and the fix are shown [in this playground link](https://tsplay.dev/weLBaW). Does that fully address the question? If so I'll write up an answer explaining; if not, what am I missing? – jcalz Aug 20 '23 at 02:42
  • All squares are rectangles, but not all rectangles are squares. So you cannot assign a rectangle to something expecting a square. In this stupid example rectangles are `string` and squares are `'a' | 'b'`. If `string` was assignable to `'a' | 'b'`, then you could do this `const input: string = prompt(); const ab: 'a' | 'b' = input`, which would be bad. `string` means any string, `'a' | 'b'` means one of two specific strings. – Alex Wayne Aug 20 '23 at 02:59
  • @jcalz Thanks, that does work. Unfortunately, I have another issue in the same vein, but I cannot seem to recreate it within Typescript Playground to showcase. But thanks, again. – mcclosa Aug 20 '23 at 03:22
  • No don't worry, your answer resolves this issue, the other issue is somewhat separate, it just uses the `DataTableColumnFiltersState` interface – mcclosa Aug 20 '23 at 13:27

1 Answers1

1

The error message

Type 'string' is not assignable to type '"string" | "union" | "of"'.

is correct, because string is wider than any string literal type or union of such types. You can't make this assignment safely:

declare const s: string; 
const u: "string" | "union" | "of" = s; // error!

After all, s could turn out to be any string whatsoever, like "oops", and then you've apparently assigned the value "oops" to a variable that expects either "string", "union", or "of". That's why string is not assignable to a union of string literals.

The reverse assignment wouldn't be a problem, and is probably what confused you. A union of string literals is definitely assignable to string, and the following assignment is fine:

declare const u: "string" | "union" | "of";
const s: string = u; // okay

But that's not what's happening in your example code.


The reason you're getting that error is because SetColumnFilters has to ultimately accept things whose id property is any string. If you try to narrow the id property of the things it accepts, you have to widen the type of SetColumnFilters, not narrow it. This is because function calls reverse the direction of assignment, and thus reverse the direction of assignability. If I ask for a string, I will be happy with "of". But if I ask for a function that accepts string, I will be unhappy with a function that only accepts "of".

(A dog named "Fluffy" is a perfectly reasonable pet, but a veterinarian who only tends to dogs named "Fluffy" would be a completely unreasonable veterinarian, and his protests of "Isn't a dog named 'Fluffy' a pet?" wouldn't change that.)

This direction reversal is known as contravariance as it contra-varies, or varies in the opposite way. See Difference between Variance, Covariance, Contravariance and Bivariance in TypeScript for more information.


As a demonstration: your setColumnFilters expects to ultimately receive things whose id property is one of the three strings "union", "of", or "string". So it should be safe for it to use that id to look up a value in some object with those keys:

const val = { union: 0, of: 1, string: 2 }
const setColumnFilters: OnChangeFn<FilterState[]> =
  (x) => {
    if (typeof x !== "function")
      x.map(e => val[e.id].toFixed(2))
  };

Let's imagine that

const props: Props = { setColumnFilters }

compiles with no error. Now since Props is defined in terms of SetColumnFilters, which is defined where that id type is just string, the following use of props is also considered safe by the compiler:

props.setColumnFilters([
  { id: "oops", value: "" }
]);

If you actually run that, you'll get a runtime error like TypeError: val[e.id] is undefined. Clearly something bad happened. But the implementation of setColumnFilters adheres perfectly to its contract, and so does the call to props.setColumnFilters. The bad thing was in the compiler allowing you to assign { setColumnFilters } to Props. There should be an error there warning you about this possibility.

And there is indeed such an error! If you want that to compile with no error you can use a type assertion as you mention, but that lack of type safety would be your responsibility at that point.


If you want to "fix" this without losing type safety then you'll probably need to make your types generic so that they can mention the specific subtype of ColumnFilter they expect. Maybe like this:

type SetColumnFilters<T extends TableState['columnFilters']> =
  OnChangeFn<T>;
interface Props<T extends TableState['columnFilters']> {
  setColumnFilters: SetColumnFilters<T>;
}

Now props would be of type Props<FilterState[]>, which compiles with no error:

const props: Props<FilterState[]> = { setColumnFilters } // okay

And now the above scenario would give you in error in the call to props.setColumnFilters(), which is probably where you want such an error to be:

props.setColumnFilters([
  { id: "oops", value: "" } // error!
]);
// Type '"oops"' is not assignable to type '"string" | "union" | "of"'.

This prompts you to fix the issue by changing id from "oops" to one of the three supported strings:

props.setColumnFilters([
  { id: "union", value: "" }
]); // okay

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360