I don’t understand why TypeScript gives me this error. Please help me fix it.
type Admin = {
id: string
email: string
password: string
age: number
isMaster: boolean
// ...
}
type Student = {
id: string
name: string
email: string
age: number
isActive: boolean
// ...
}
type PropertyTypes<T> = T[keyof T]
type PropertySelector<T extends {}> = {
[K in keyof T]: (k: K) => T[K]
}
interface BookshelfEntity<T extends {}> {
get: PropertyTypes<PropertySelector<T>>
toJSON: () => T
}
type User = BookshelfEntity<Student> | BookshelfEntity<Admin>
const getStudentId = (user: User): string => user.get('id')
// ^^^
// Type 'string | number | boolean' is not assignable to type 'string'.
// Type 'number' is not assignable to type 'string'.(2322)
// Argument of type 'string' is not assignable to parameter of type 'never'.(2345)
>Solution :
Try this to get some insight:
type Test = PropertyTypes<PropertySelector<Student>>
This resolves to:
type Test = ((k: "id") => string) | ((k: "name") => string) | ((k: "email") => string)
So this is a union type of functions that each accept a particular string constant, but it doesn’t tell us which of those function types is actually contained in a variable of type Test. So, to be able to call it, you need to provide an argument that is correct for all those signatures, i.e. a string that is equal to "id" and to "name" and to "email". This is obviously impossible, so the argument type is resolved to never.
You can solve this particular problem by declaring the interface like this:
interface BookshelfEntity<T extends {}> {
get<K extends keyof T>(k: K): T[K],
toJSON: () => T
}
Now get is a single function with a generic signature, and its return value depends on its argument as you’d want.
However, you’ll find that this is not enough:
type User = BookshelfEntity<Student> | BookshelfEntity<Admin>
const getStudentId = (user: User): string => user.get('id')
The error is:
This expression is not callable.
Each member of the union type(<K extends keyof Student>(k: K) => Student[K]) | (<K extends keyof Admin>(k: K) => Admin[K])has signatures, but none of those signatures are compatible with each other.
I’m not sure why this is happening; it may be a limitation on when or how generics are resolved to concrete types.
A workaround is to declare User slightly differently:
type User = BookshelfEntity<Student | Admin>
Now User.get has signature <K extends "id" | "email">(k: K) => (Admin | Student)[K], i.e. any field that Student and Admin have in common. And the return value is the union of the return values that can be produced for that argument, i.e. if Admin has id: number and Student has id: string, then get('id') will have type number | string.