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'
}
}
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'
}
}
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';
}
We can ask if Admin
extends truly the User
:
type IsAdminAUser = Admin extends User ? true : false // evaluates to true
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)
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.
Top comments (8)
Good explanation, thanks!
Thanks for the good examples.
for the bug, i would say, we are overriding "kind" member with 'customer' no matter what. So the solution could be like checking if the kind member given by the argument. I would not call this as bug, i was expecting this behavior actually :)
but also the important thing is what is the expectation from the function. function says, makeCustomer, does not matter we are assigning to kind 'admin', 'hooman', or 'animal', eventually it sounds like we are making a customer :)
yet, it was good example :)
Thank you for putting together these exercises, they are really great for learning purposes!
I have a question regarding the bug you mention please. Isn't TS assuming the wrong
kind
because inmakeCustomer
you are overwriting the kind to becustomer
?Wouldn't the following solve the bug?
That would indeed fixed the issue, but the idea was to have constructor of the customer. Besides sense of this function (which is discussable) we can fix it by saying we will return specific kind. Pay attention that I needed to omit kind property from Admin type as having a join between {kind:'a'} & {kind: 'b'} creates never type as there is no intersection between them. Here is the code - typescriptlang.org/play?#code/C4Tw...
Cool, I didn't realize about the bug you mentioned, makes sense, thanks for sharing :)
Thanks for the great answer you have given!
A pleasure, it's a great way to improve my knowledge about TS.
I will be recommending this series to some of my colleagues.
Type is
Admin
but the structure which is returned haskind
property with valuecustomer
, so its not valid member of the typeAdmin
.