Issue
I have access to an external library that looks like the following:
type ParamMap = {
a: "a1" | "a2";
b: "b1" | "b2";
};
type Params<T extends keyof ParamMap> = ParamMap[T];
export const fn = <Namespace extends keyof ParamMap, Val extends Params<Namespace>>(
namespace: Namespace,
key: Val
): string => `${namespace}.${key}`;
Is there a way to get the type of key
when namespace
is a
with only access to fn
? I assume it would look something like: Parameters<fn>['a', infer T]
but I can't get it to work.
Solution
You can't really do this directly because the type you care about is a constraint on the Val
type parameter, and there's currently no way to infer a generic constraint. See microsoft/TypeScript#41040.
The only way I can think of to do this is to jump through a lot of hoops both with types and with expressions. These expressions will be emitted to JavaScript, so you will have some code at runtime that serves absolutely no purpose.
First, let's start with typeof fn
, which I'll give the name Fn
here:
type Fn = <
Namespace extends keyof ParamMap, Val extends Params<Namespace>
>(namespace: Namespace, key: Val) => string
We want to find out the required type of key
is when namespace
is "a"
. But the TypeScript type system lacks the sort of expressiveness necessary to directly ask this question. Conceptually you could imagine specifying that Namespace
is "a"
, but you can't do that without also specifying Val
(there is no "partial" type argument inference; see microsoft/TypeScript#26242). You could also try to just call fn("a", ???)
, but there's nothing good to put in for ???
; if you put in something random then Val
will be inferred as the type of that random thing, and you'd probably get a compiler error.
It would be nice if we could call a curried version of fn
, of the type:
type CurriedFn = <
Namespace extends keyof ParamMap, Val extends Params<Namespace>
>(t: Namespace) => (u: Val) => string
Then you'd call curriedFn("a")
and it would necessarily return a new function of type (u: Params<"a">) => string
, which you could then probe with the Parameters<T>
utility type.
One hiccup is that you can't curry arbitrary function types purely at the type level. That would require TypeScript to have higher kinded generic types, such as those requested in microsoft/TypeScript#1213. Luckily there is support for such type manipulations of generic function values. So instead of acting on Fn
we need to act on fn
... or on some other value that TypeScript believes is of type Fn
.
So we need a function to represent the transformation, like this:
const curry = <T, U, R>(
fn: (t: T, u: U) => R
) => (t: T) => (u: U) => fn(t, u);
And then if we call it on fn
, we get the generic type we wanted:
const curriedFn = curry(null! as Fn);
// ^? const curriedFn: <
// Namespace extends keyof ParamMap, Val extends Params<Namespace>
// >(t: Namespace) => (u: Val) => string
Hooray!
An aside about how to proceed if we don't have direct access to fn
. We don't really care about fn
at runtime, just as long as TypeScript believes we have a value of type Fn
. We can pretend to do that with a type assertion.
Like, here a random function is asserted:
const curriedFn = curry((() => { }) as Function as Fn);
which works but is a bit verbose. We don't need an actual function:
const curriedFn = curry(0 as any as Fn);
This is still verbose because of the intermediate assertion. The tersest way to pretend we have a value of an arbitrary type is probably:
const curriedFn = curry(null! as Fn);
That works since null!
uses the non-null assertion operator, it's saying "a non-null
null
", which can't exist, so it's of type never
, which can be asserted to any type. "null!
" is 5 characters long, and I don't think I've seen anything shorter.
All of those result in the same type.
And now we can call this with "a"
as an argument, and it will compile:
const takeKey = curriedFn("a");
// ^? const takeKey: (u: "a1" | "a2") => string
And the first parameter of that function is the type we want:
type Key = Parameters<typeof takeKey>[0];
// ^? type Key = "a1" | "a2"
Hooray again.
So, there you go. We've at least somewhat programmatically extracted "a1" | "a2"
from Fn
. You can also write curriedFn("b")
to find "b1"| "b2"
.
But it's got observable side effects. It emits the following JavaScript code:
const curry = (fn) => (t) => (u) => fn(t, u);
const curriedFn = curry(null);
const takeKey = curriedFn("a");
which is useless and just weird. Maybe that's not a huge problem for some use cases, but it's not ideal.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.