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> {
// ...
}
Top comments (0)