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)