I'm experiencing some unexpected type inference behavior in TypeScript when trying to convert a three-argument function to a single-argument function which takes an object.
I am trying to define a solid-js
(or React) component that behaves similarly to the applyWhen
function below, which returns a JSX.Element
. This problem originates from an attempt to generalize this workaround.
First, we define a Result
discriminated union type and a type predicate for each branch:
type ResultOk<T> = { status: "ok", data: T };
type ResultErr<E> = { status: "error", error: E };
type Result<T,E> = ResultOk<T> | ResultErr<E>;
const isOk = <T,E>(result: Result<T, E>): result is ResultOk<T> => {
return result.status === "ok";
}
const isErr = <T,E>(result: Result<T, E>): result is ResultErr<E> => {
return result.status === "ok";
}
We also define the type Predicate
of type predicates:
type Predicate<S extends T, T> = (e:T) => e is S;
Now, we want to write a function applyWhen
that applies a function to a value, provided that the value satisfies a given type predicate. The simplest definition is:
// Version 1: Pass Three Parameters
const applyWhen_params = <S extends T, T, R>(
t: T,
pred: Predicate<S, T>,
fn: (s: S) => R
): R|null => {
if(pred(t)) {
return fn(t);
} else {
return null;
}
}
// Example 1: Success! No Type Errors!
function example_params(x: Result<number, string>) {
applyWhen_params(
x,
isOk,
(a) => { return a.data + 1; }
)
}
Hovering over the call to applyWhen_params
, we see that the generic arguments S
and T
are correctly inferred:
// inferred type
const applyWhen_params: <ResultOk<number>, Result<number, string>, number>(t: Result<number, string>, pred: Predicate<ResultOk<number>, Result<number, string>>, fn: (s: ResultOk<...>) => number) => number | null
The above example no longer type-checks if we pass the three values as an object, rather than one-by-one as arguments:
// Version 2: Pass an Object
const applyWhen_props = <S extends T, T, R>(props: {
t: T,
pred: Predicate<S, T>,
fn: (s: S) => R
}): R|null => {
if(props.pred(props.t)) {
return props.fn(props.t);
} else {
return null;
}
}
// Example 2: Type Error!
function example_props(x: Result<number, string>) {
applyWhen_props({
t: x,
pred: isOk,
fn: (a) => { return a.data + 1; }
// ^^^^
// TypeError: Property 'data' does not exist on type 'Result<number, string>'.
})
}
Hovering over the call to applyWhen_props
shows that the inferred type is too general:
// inferred type of applyWhen_props
const applyWhen_props: <Result<number, string>, Result<number, string>, any>(props: {
t: Result<number, string>;
pred: Predicate<Result<number, string>, Result<number, string>>;
fn: (s: Result<...>) => any;
}) => any
// inferred type of pred
pred: Predicate<Result<number, string>, Result<number, string>>
// inferred type of isOk
const isOk: <T, E>(result: Result<T, E>) => result is ResultOk<T>
Question
Is there a way to modify example_props
so that it type-checks, without having to explicitly annotate the type parameters?