I don't think the TypeScript compiler has enough support for higher order type analysis to do this for you. The problems I see:
There's no good way for the compiler to infer that the function const mapper = (v: 1 | 2) => v === 1 ? "aaa" : "bbb"
is of a conditional generic type like <V extends 1 | 2>(v: V) => V extends 1 ? "aaa" : "bbb"
or of an overloaded function type like {(v: 1): "aaa", (v: 2): "bbb"}
. If you want the compiler to treat the function like that you will have to assert or annotate the type manually.
Even if it could infer that, there's no way to write a type function like Apply<typeof f, typeof x>
where f
is an overloaded or generic function of one argument, and x
is an acceptable argument for it so that Apply<typeof f, typeof x>
is the type of f(x)
. Shorter: there's no typeof f(x)
in TypeScript. So while you can call mapper(1)
and the compiler knows the result is of type "aaa"
, you can't represent that knowledge in the type system. That prevents you from doing something like overloaded function resolution or generic function resolution in the type system.
The most straightforward typing I can imagine for _.mapValues
would give you the wide Record
-like type, and you'd have to assert a narrower type if you want that:
declare namespace _ {
export function mapValues<T, U>(
obj: T,
fn: (x: T[keyof T]) => U
): Record<keyof T, U>;
}
const obj = { a: 1, b: 2 } as const;
type ExpectedRet = { a: "aaa"; b: "bbb" };
_.mapValues(obj, v => (v === 1 ? "aaa" : "bbb")); // Record<"a"|"b", "aaa"|"bbb">
const ret = _.mapValues(obj, v => (v === 1 ? "aaa" : "bbb")) as ExpectedRet;
Otherwise you have to jump through many flaming hoops (manually specifying types, manually declaring functions as overloads) and you end up with something not much safer than a type assertion and a lot more complicated:
type UnionToIntersection<U> = (U extends any
? (k: U) => void
: never) extends ((k: infer I) => void)
? I
: never;
declare namespace _ {
export function mapValues<T, U extends Record<keyof T, unknown>>(
obj: T,
fn: UnionToIntersection<{ [K in keyof T]: (x: T[K]) => U[K] }[keyof T]>
): U;
}
function mapper(v: 1): "aaa";
function mapper(v: 2): "bbb";
function mapper(v: 1 | 2): "aaa" | "bbb" {
return v === 1 ? "aaa" : "bbb";
}
const obj = { a: 1, b: 2 } as const;
type ExpectedRet = { a: "aaa"; b: "bbb" };
const ret = _.mapValues<typeof obj, ExpectedRet>(obj, mapper);
Not sure if that's even worth explaining... you have to manually specify the input and expected output types in the call to _.mapValues
because there's no way the compiler can infer the output type (as mentioned above). You have to manually specify that mapper
is an overloaded function. The typing for _.mapValues
is complicated and uses UnionToIntersection
to describe the required overloaded function as the intersection of function types taking input values to output values.
So, I'd stay away from this and just use a type assertion.
Hope that helps; sorry I don't have a more satisfying answer. Good luck!
Link to code