Trying to enforce an optional, defaulted parameter on a generic method that must be a property of the passed object. Here is an example:
function doIt<T>(target: T, propA: keyof T, propB: keyof T): string {
return `${target[propA]} ${target[propB]}`;
}
But, in the above example, I would like to default propA and propB to foo and bar for example:
function doIt<T>(target: T, propA: keyof T = 'foo', propB: keyof T = 'bar'): string {
return `${target[propA]} ${target[propB]}`;
}
But when I do that, it complains that Type '"bar"' is not assignable to type 'keyof T'.ts(2322) which makes sense.
Even if I add a default type to T, it still doesn't work:
function doIt<T = { foo: string; bar: string }>(target: T, propA: keyof T = 'foo', propB: keyof T = 'bar'): string {
return `${target[propA]} ${target[propB]}`;
}
Basically, I want to be able to do:
doIt({foo: 'jane', bar: 'doe'}); // should return 'jane doe'
doIt({zoo: 'jane', baz: 'doe'}); // should fail compilation
doIt({zoo: 'jane', baz: 'doe'}, 'zoo', 'baz'}); // should return 'jane doe'
Any ways of doing this?
You can't easily use default parameter values for this without running afoul of the type checker.
The simplest way to get what you're looking for is probably to use overloads to describe the different intended ways to call doIt(), like this:
namespace Overloads {
function doIt<T>(target: T, propA: keyof T, propB: keyof T): string;
function doIt<T extends { bar: any }>(target: T, propA: keyof T): string;
function doIt<T extends { foo: any; bar: any }>(target: T): string;
function doIt(target: any, propA?: keyof any, propB?: keyof any): string {
if (typeof propA === "undefined") propA = "foo";
if (typeof propB === "undefined") propB = "bar";
return `${target[propA]} ${target[propB]}`;
}
console.log(doIt({ foo: "jane", bar: "doe" })); // jane doe
console.log(doIt({ zoo: "jane", baz: "doe" })); // error!
// ┌─────────────> ~~~~~~~~~~~~
// "zoo" not expected (although I'm surprised it doesn't complain about missing foo/bar)
console.log(doIt({ zoo: "jane", baz: "doe" }, "zoo", "baz")); // jane doe
console.log(doIt({ zoo: "jane", bar: "doe" }, "zoo")); // jane doe
}
That works the way you want. Note that the three call signatures correspond to different numbers of parameters, and they each have different constraints on the generic type parameter T. (You may wish to use string instead of any as property types, there... not sure)
Note that in the implementation signature I've got target as any, propA, and propB as (keyof any) | undefined. So the implementation won't be particularly type-safe here. Overloads don't really help with implementation type safety; they are more for the caller's benefit. If you tried to make the implementation generic you'd run into similar problems where the compiler can't verify that "foo" is assignable to keyof T, etc.
Also note that we can't use default parameter values in overload signatures, but that's fine... we can just do that assignment ourselves inside the function implementation. If the parameters are undefined, reassign them.
Another way to do this is to use TypeScript's support for treating rest parameters as tuple types, including optional tuple elements:
namespace ConditionalTupleParameters {
function doIt<T>(
target: T,
...[propA, propB]: T extends { foo: any; bar: any }
? [(keyof T)?, (keyof T)?] // propA and propB are optional
: T extends { bar: any }
? [keyof T, (keyof T)?] // propA is required, propB is optional
: [keyof T, keyof T] // propA and propB are optional
): string {
if (typeof propA === "undefined") propA = "foo";
if (typeof propB === "undefined") propB = "bar";
return `${target[propA]} ${target[propB]}`;
}
console.log(doIt({ foo: "jane", bar: "doe" })); // jane doe
console.log(doIt({ zoo: "jane", baz: "doe" })); // error!
// ┌──────> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// expected 3 arguments, got 1
console.log(doIt({ zoo: "jane", baz: "doe" }, "zoo", "baz")); // jane doe
console.log(doIt({ zoo: "jane", bar: "doe" }, "zoo")); // jane doe
}
This works similarly to the overloads. It's fairly ugly looking, which is not great. But, the implementation is actually relatively type-safe, where target is still known to be type T, and propA and propB end up being keyof T after checking against undefined.
Okay, hope one of those helps. Good luck!
Link to code
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