Reputation: 2737
Given the example below, are there any ways to achieve DRY code that's similar to foobarA
?
interface F<T, U extends string> { t: T, f: (u: U) => void }
declare const foo: <T, U extends string>(type: U) => F<T, U>;
// the following correctly types foobar, however 'bar' redundantly repeats.
const foobar = foo<string, 'bar'>('bar')
// ideally one would want the generic inference to be smarter and the
// following to work:
const foobarA = foo<string>('bar')
Upvotes: 3
Views: 255
Reputation: 329678
TypeScript does not directly support partial type argument inference as requested in microsoft/TypeScript#26242. So if you have a generic function of the form:
declare const foo: <T, U extends string>(u: U) => F<T, U>;
there is no way to call it so that you manually specify T
but let the compiler infer U
:
// const foobarA: F<string, unknown> ☹
const foobar = foo<string, "bar">("bar");
Either you have to manually specify both type arguments as in foo<string, "bar">("bar");
(which is unfortunately redundant) or you let the compiler infer both arguments as in foo("bar")
(which doesn't infer the right type for T
).
Until and unless this changes, you will need to work around it.
One workaround is currying, whereby you split the single generic function call into multiple generic function calls, so that you can manually specify the type argument for the first, and then let the compiler infer the type argument for the second. That means we'd need to refactor the above foo()
into
declare const foo: <T, >() => <U extends string>(u: U) => F<T, U>;
And then we could call the function like this:
declare const foo: <T, >() => <U extends string>(u: U) => F<T, U>;
const foobarA = foo<string>()("bar");
// const foobarA: F<string, "bar">
It's a bit annoying to have to throw in that extra function call, but syntactically at least it's just another pair of parentheses, and it gives you exactly the behavior you're looking for.
Another workaround is to add a dummy parameter corresponding to the type argument you'd like to specify manually, and then pass in a value of that type so that the compiler can successfully infer it. That looks like this:
declare const foo: <T, U extends string>(dummyT: T, u: U) => F<T, U>
And then we could call the function like this:
const foobarA = foo("someString", "bar");
// const foobarA: F<string, "bar">
The value "someString"
passed in for dummyT
is almost certainly ignored by the function implementation, but it allows the compiler to infer string
for T
. If the caller doesn't have a value of the type handy, they can still use the dummy version by asserting that something random is of that type:
const foobarB = foo(null! as string, "bar");
// const foobarB: F<string, "bar">
Here I've used null!
, the value is null
and by asserting that it is non-null
via postfix !
, we have an impossible "non-null null" of the never
type which can be assigned to any other type. The main benefit of a mind-bending thing like null!
is that it takes only five keystrokes to produce a value of the impossible never
type, which can be asserted as string
without complaint by the compiler.
Both versions work; I tend to favor currying unless the use case makes it easy for the caller to find and pass in an appropriate argument for the dummy parameter.
Upvotes: 6
Reputation: 44416
The following will literally “work” — it will compile. I don’t know if it will actually accomplish what you want.
const foo = <S,T extends string = string>(type:T)=>{
return {
[type]: (arg: S)=>console.log(arg)
}
}
Upvotes: 0