Advanced TypeScript: Overloading With Options Objects
Welcome to the Overloading With Options Objects lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
In an earlier lesson, we saw function overloads. In a different lesson, we saw a way to statically type "options" objects passed to functions. Now we'll combine the two, which enables powerful static typing of complex functions.
Our example concerns database querying. The example below defines a function
findUser(id: number): User. It retrieves a user object from the "database", then returns the object.As usual, we won't actually use a database. Instead, we'll return hard-coded user objects. We want to focus on the types, not the database.
>
function findUser(id: number) {return {id: 1, name: 'Amir'};}findUser(1);Result:
{id: 1, name: 'Amir'}Real software systems tend to outgrow simple query functions like
findUser, mostly due to performance problems. Sometimes we need the whole user object. Sometimes we only need one of its properties or columns. Sometimes we need the user plus every comment that they've written.In our
Usertype below, the user'scommentsproperty is optional. When we pass{withComments: true}to our function, the returned user has acommentsproperty. Otherwise, there's nocommentsproperty at all.>
type Comment = {subject: string};type User = {id: numbername: stringcomments?: Comment[]};function findUser(id: number,options: {withComments?: boolean} = {}): User {const user = {id: 1, name: 'Amir'};if (options.withComments) {return {...user, comments: [{subject: "Ms. Fluff's 4th birthday"}]};} else {return user;}}- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
findUser(1);Result:
{id: 1, name: 'Amir'} Here's a code problem:
In the code below, we call
findUserwithout providing any options. Modify the call so that our query includes comments as well.type Comment = {subject: string};type User = {id: numbername: stringcomments?: Comment[]};function findUser(id: number,options: {withComments?: boolean} = {}): User {const user = {id: 1, name: 'Amir'};if (options.withComments) {return {...user, comments: [{subject: "Ms. Fluff's 4th birthday"}]};} else {return user;}}findUser(1, {withComments: true});- Goal:
{id: 1, name: 'Amir', comments: [{subject: "Ms. Fluff's 4th birthday"}]}- Yours:
{id: 1, name: 'Amir', comments: [{subject: "Ms. Fluff's 4th birthday"}]}
All of that code works just fine, but our
commentsproperty has an awkward type. It's optional in our object type, so its effective type is alwaysComment[] | undefined. When we callfindUser(1, {withComments: true}), we still have to handle thatundefinedcase. That can lead to many unnecessary conditionals because our types are too general.We either provide
{withComments: true}or not, so thecommentsproperty is either there or it's not. It would be nice if the type reflected that. With function overloads, we have the power to define that type! We can overloadfindUserto return different types depending on theoptionsobject.Here's the implementation. When we provide
{withComments: true}, we get aUserWithCommentsback. When we don't provide it, we get a regularUserback.>
type Comment = {subject: string};interface User {id: numbername: string}interface UserWithComments extends User {comments: Comment[]}function findUser(id: number): User;function findUser(id: number, options: {withComments: true}): UserWithComments;function findUser(id: number,options: {withComments?: boolean} = {}): User | UserWithComments {const user = {id: 1, name: 'Amir'};if (options.withComments) {return {...user, comments: [{subject: "Ms. Fluff's 4th birthday"}]};} else {return user;}}If we don't provide
{withComments: true}, then trying to accesscommentsis a type error.- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
const user = findUser(1);user.name;Result:
'Amir'
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
const user = findUser(1);user.comments[0].subject;Result:
type error: Property 'comments' does not exist on type 'User'.
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
const user = findUser(1, {withComments: true});user.comments[0].subject;Result:
"Ms. Fluff's 4th birthday"
Now an important question: should we spend this much effort to avoid the
Comment[] | undefinedproblem? As usual, the answer is "it depends".As we saw in the original function overload lesson, overloads do introduce a type safety hole in the body of the function. And as we can see above, they also complicate our type definitions.
Our recommendation is to use overloads only in situations where there's a lot of awkward code duplication. For example, if we find ourselves writing
if (user.comments !== undefined) { ... }in hundreds of places, and we can't find any other way to remove them, then an overload may be a good idea. But if we're only writing that conditional in a few places, then it's probably better to keep the function simple, avoid overloads, and accept those few extra conditionals.