I'm trying to write a function where the return type is specified by a string, but with a few extra twists thrown in:
Can this be done?
Here's a stripped down version of what I'm trying to do. In the examples below, I show the different ways I want to be able to call my function fn() and have TypeScript correctly infer the return type:
interface NumberWrapper {
type: "my_number";
value: number;
}
const x1: number = fn(1, { returnType: "bare" });
const x2: NumberWrapper = fn(1, { returnType: "wrapped" });
const x3: void = fn(1, { returnType: "none" });
const x4: number = fn(1, {});
const x5: number = fn(1);
For x1, x2, x3, the function is called with a value for returnType.
For x4, no value is provided for returnType, and I want it to default to "bare". Similarly, for x5, the object isn't provided at all, and I want it to behave as though returnType is "bare".
(You might be wondering why I'm bothering to put returnType in an object, since it's just one parameter. In my actual use case, there are other parameters in the object.)
So far I've been able to get x1, x2, and x3 to work, but not x4 or x5. Here' what I have:
type ReturnMap<T extends "bare" | "wrapped" | "none"> = T extends "bare"
? number
: T extends "wrapped"
? NumberWrapper
: void;
function fn<T extends "bare" | "wrapped" | "none">(
x: number,
{ returnType }: { returnType: T }
): ReturnMap<T> {
if (returnType === "bare") {
return x as ReturnMap<T>;
} else if (returnType === "wrapped") {
return { type: "my_number", value: x } as ReturnMap<T>;
} else {
return undefined as ReturnMap<T>;
}
}
One thing I dislike is that each return statement has the form return x as ReturnMap<T> at the end. I'd like to not do that because the as causes it to lose some type safety.
But the bigger problem is that this doesn't work for x4 and x5. I've tried using default values in different ways, but haven't been able to get it to work.
A quick fix is to make the generic type the whole argument rather than only the returnType property. Then it's just a (longer) conditional operator chain to get the types you want - though this still requires the ugly as assertions.
type Arg = undefined | { returnType?: "bare" | "wrapped" | "none" };
type ReturnMap<T extends Arg> =
T extends { returnType: 'wrapped' }
? NumberWrapper
: T extends { returnType: 'none' }
? void
: number;
function fn<T extends Arg>(
x: number,
arg?: T
): ReturnMap<T> {
if (!arg || !('returnType' in arg) || arg.returnType === 'bare') {
return x as ReturnMap<T>;
} else if (arg.returnType === 'wrapped') {
return { type: "my_number", value: x } as ReturnMap<T>;
} else {
return undefined as ReturnMap<T>;
}
}
const x1: number = fn(1, { returnType: "bare" });
const x2: NumberWrapper = fn(1, { returnType: "wrapped" });
const x3: void = fn(1, { returnType: "none" });
const x4: number = fn(1, {});
const x5: number = fn(1);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With