DEV Community

loading...

Advanced TypeScript Exercises - Answer 2

macsikora profile image Maciej Sikora Updated on ・2 min read

In the question I have asked why below snippet doesn't compile

type User = {
  id: number;
  kind: string;
};

function makeCustomer<T extends User>(u: T): T {
  // Below error, why?
  return {
    id: u.id,
    kind: 'customer'
  }
}
Enter fullscreen mode Exit fullscreen mode

At the first look object which is returned by makeCustomer is valid User type as it has both needed fields defined in the User. The crucial thing to understand is that we work here with type variable T which extends from User but it doesn't mean it is User. T is assignable to User, so it needs to have all fields which User has, but, it can have more fields!

Yes and this is exactly the issue, returned object is a User and pass all constraints of it, but doesn't pass all constraints of T which can have additional fields. We don't know what are those fields though, so in order to fix the typing we should make an object which has all fields of T, and we know all fields of T are in argument u. We can then use spread operator in order to spread all unknown fields to the newly created object.

function makeCustomer<T extends User>(u: T): T {
  // no error
    return {
    ...u, // spread all properties of u being T
    id: u.id, // yes redundant line, leaving it for consistency
    kind: 'customer'
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we are sure that all fields from T will be included in our returned object. But there is also a case where TypeScript has an issue, design issue probably. We can create here situation which will be just a bug, when value will not match the type. Consider that we can make another type which will extend User, lets say Admin

type Admin = User & {
  kind: 'admin';
}
Enter fullscreen mode Exit fullscreen mode

We can ask if Admin extends truly the User:

type IsAdminAUser = Admin extends User ? true : false // evaluates to true
Enter fullscreen mode Exit fullscreen mode

Ok so Admin is a User, then we can use makeCustomer with Admin? Looks that we should as T extends User and Admin extends User. Lets check:

const admin = makeCustomer({ id: 1, kind: 'admin' } as Admin)
Enter fullscreen mode Exit fullscreen mode

And there is a bug, TS is saying admin has a type Admin but when we console.log it, the structure is {id: 1, kind: 'customer'}, so unfortunately we got to the situation where TS has wrong assumptions 😩.

The whole code can be found in the playground

This series is just starting. If you want to know about new exciting questions from advanced TypeScript please follow me on dev.to and twitter.

Discussion

pic
Editor guide
Collapse
nombrekeff profile image
Manolo Edge

Cool, I didn't realize about the bug you mentioned, makes sense, thanks for sharing :)

Collapse
macsikora profile image
Maciej Sikora Author

Thanks for the great answer you have given!

Collapse
nombrekeff profile image
Manolo Edge

A pleasure, it's a great way to improve my knowledge about TS.

I will be recommending this series to some of my colleagues.

Collapse
lukaszahradnik profile image
Lukáš Zahradník

But admin should have type Admin.

Argument of the function is type T and so is the return type. In your example is the type of argument Admin, therefor is the return type also Admin.

Collapse
macsikora profile image
Maciej Sikora Author

Type is Admin but the structure which is returned has kind property with value customer, so its not valid member of the type Admin.

Collapse
lukaszahradnik profile image
Lukáš Zahradník

Yes, it's not valid. But admin should always be type of Admin, or an error should occur (like in this case).