Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

TypeScript doesn't understand this function with this argument gives this type

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 :

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

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.

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading