TypeScript Tip: Using Conditional Types To Refactor Overloads.
Anderson. J
Posted on April 4, 2022
Introduction
A few days ago I was refactoring code and I found something like this:
interface IRawUser {
first_name: string
email: string,
id: number,
}
interface User {
name: string,
email: string
print: () => void
}
declare function userFactory(rawUser: IRawUser): User;
function mapRawToUserObject(rawShow: IRawUser[]): User[];
function mapRawToUserObject(rawShow: IRawUser): User;
function mapRawToUserObject(rawShow: IRawUser | IRawUser[]): User | User[] {
if (rawShow instanceof Array) {
return rawShow.map((raw) => userFactory(raw));
}
return userFactory(rawShow);
}
The function mapRawToUserObject
uses overloads to express the following: If we call it with an array of IRawUser[]
it will return an array of User[]
, if we call it with a single IRawUser
it will return a single User
.
Nothing too complex but it seemed like a good opportunity to refactorize the method definition.
Creating a conditional type.
We need to take a decision on the type of input that the method receives.
The possible inputs we need to look at are: IRawUser
and IRawUser[]
type MapRawResult<T extends IRawUser | IRawUser[]> = any;
Then we add the conditional logic:
type MapRawResult<T extends IRawUser | IRawUser[]> = T extends IRawUser ? User : User[]
In plain words: If
T
is assignable toIRawUser
returnUser
otherwise returnUser[]
Applying the conditional type to our method.
Right now our method looks like this:
function mapRawToUserObject(rawShow: IRawUser[]): User[];
function mapRawToUserObject(rawShow: IRawUser): User;
function mapRawToUserObject(rawShow: IRawUser | IRawUser[]): User | User[] {
// implementation
}
We can now remove the overloads, add a generic parameter (T extends IRawUser | IRawUser[]
), and replace the return type with the one we recently created.
type MapRawResult<T extends IRawUser | IRawUser[]> = T extends IRawUser ? User : User[]
function mapRawToUserObject<T extends IRawUser | IRawUser[]>(rawShow: T): MapRawResult<T> {
// implementation
}
At this point, typescript will complain about the type of value that we are returning.
The compiler can't infer the type of the value we are returning, a solution for this is to explicitly express the type we are expecting using the keyword as
.
Our final method then looks like this:
function mapRawToUserObject<T extends IRawUser | IRawUser[]>(rawShow: T): MapRawResult<T> {
if (rawShow instanceof Array) {
return rawShow.map((raw) => userFactory(raw)) as MapRawResult<T>
}
return userFactory(rawShow) as MapRawResult<T>
}
Conclusion
If we try with some values, we can see how our method behaves the same as before but without using overloads.
As a bonus we could use a type alias for IRawUser | IRawUser[]
and make the code cleaner:
type MapRawArg = IRawUser | IRawUser[];
type MapRawResult<T extends MapRawArg> = T extends IRawUser ? User : User[];
function mapRawToUserObject<T extends MapRawArg>(rawShow: T): MapRawResult<T> {
// ...
}
Further Reading
Posted on April 4, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 28, 2024
October 16, 2024