Reputation: 2527
Lets say I have something like this:
type QueryResult<ResultType = string, ErrorType = string> = {
result: ResultType,
} | {
errors: ErrorType,
}
Now, when I create my result, I can do something like:
const myResult: QueryResult = {
result: "My result!",
};
Also, when I want to change the ResultType
, I can add it to the generic:
const myResult: QueryResult<number> = {
result: 10,
errors: null
};
However, what if I want to change the type value for errors
? Well, I could do this:
// This works, but I don't want to specify the default value
const myErrorResult: QueryResult<string, MyErrorType> = {
errors: {
location: [1, 2],
message: "Invalid argument!",
}
};
How can I skip specifying the ResultType
? Is that even possible? The main reason for this is that if I have lots of generics, I don't want to provide the default values for every generic type. I've tried QueryResult<, string>
, but that throws an error, as well as QueryResult<ErrorType = MyErrorType>
.
You can find a playground link here.
Thanks in advance!
NOTE: This is just an example, and I know that this isn't a very good way of representing errors/results.
Upvotes: 1
Views: 464
Reputation: 99786
jcalz's answer is amazing, but I thought I would suggest another potential workaround.
The easy might be to just create a separate type for this case:
type ErrorQueryType<ErrorType = string> = QueryType<string, ErrorType>;
Upvotes: 1
Reputation: 330086
No, there's no direct support for skipping over generic type parameters in TypeScript. See microsoft/TypeScript#10571 and microsoft/TypeScript#26242 for longstanding open feature requests for such functionality (although it's not clear in those issues whether skipping should have the compiler infer the skipped parameters, or if it should fall back to the default, or some combination of those). Right now you have to either give up on it (by QueryResult<string, MyErrorType>
) or work around it.
One possible approach for a workaround is to come up with a "marker" or "sigil" type which is used to signal that the compiler should fall back to some intended default type. For now let's call it Default
(but you can imagine calling it $
or something shorter).
It's just a marker; you don't want it to be confused with any "actual" type. Here's one way to do that:
declare class Default { private prop: string }
You will never actually have a value of type Default
. And because it's declared to be a class
with a private
property, no other types should accidentally be assignable to it.
Then we can define Fallback<T, D>
which evaluates to T
unless T
is Default
, in which case it falls back to D
:
type Fallback<T, D> = T extends Default ? D : T;
Your original QueryResult
we will rename out of the way as _QueryResult
and not specify any defaults:
type _QueryResult<R, E> = { result: R } | { errors: E };
And finally, here's QueryResult
:
type QueryResult<R = Default, E = Default> =
[Fallback<R, string>, Fallback<E, string>] extends [infer R, infer E] ?
_QueryResult<R, E> : never;
The idea is that a QueryResult<R, E>
will be just like _QueryResult<R, E>
, except if R
is Default
it will fall back to string
, and if E
is Default
it will fall back to string
also.
Let's try it out. The normal usages work similarly to before:
const myResult: QueryResult = { result: "My result!" };
// const myResult: _QueryResult<string, string>
const myGenericResult: QueryResult<number> = { result: 10 }
// const myGenericResult: _QueryResult<number, string>
But now for myErrorResult
we can write this:
const myErrorResult: QueryResult<Default, MyErrorType> = {
errors: {
location: [1, 2],
message: "Invalid argument!",
}
};
// const myErrorResult: _QueryResult<string, MyErrorType>
which falls back nicely to _QueryResult<string, MyErrorType>
. If instead of Default
you had $
, you could maybe even imagine LotsOfTypeParameters<$, $, $, string, $, $, number>
not being too cumbersome.
Of course this is just a workaround. If you were relying on type inference for QueryResult
, you might be disappointed with this version, since conditional types are hard to infer properly. It really comes down to your use cases.
Depending on what you want there may be a different version that works better for you. Or if no workaround meets your needs, you may just want to go to the existing feature requests in GitHub, give them a 👍, and describe your use case and why the only solution that works for you is a genuine way of skipping generic type parameters. I wouldn't be optimistic about that having much effect, but it wouldn't hurt.
Upvotes: 2