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