Home > Mobile >  In Typescript, how can I pass an array of (keyof Object) without having to hardcode it every time? H
In Typescript, how can I pass an array of (keyof Object) without having to hardcode it every time? H

Time:07-02

So I have the following very simple User interface and getUserById() method that retrieves a type-safe user that includes only the specified fields/properties (what is usually called a projection):

interface User {
  _id: string,
  username: string,
  foo: string
}

function getUserById<Field extends keyof User>(
  id: string,
  fields: Field[]
): Pick<User, Field> {   
  // In real-life an object with only the specified fields/properties is returned
  // and the id and fields parameters are both used as part of this process
  return {} as User; 
}

This is working perfectly as demonstrated in the usage bellow, because user1 is fully type-safe and accessing user1.foo will raise a Typescript error:

const user1 = getUserById('myId', ['_id', 'username']);
console.log(user1); // user1 is correctly typed as Pick<User, '_id' | 'username'>
console.log(user1.foo); // We get the error: Property 'foo' does not exist. Great!

However now I want to do what I thought should be a fairly easy thing. As I use the ['_id', 'username'] parameter a lot of times, I want to create a DEFAULT_USER_FIELDS constant for it so that I can re-use it everywhere:

const DEFAULT_USER_FIELDS: (keyof User)[] = ['_id', 'username'];

const user2 = getUserById('myId', DEFAULT_USER_FIELDS);
console.log(user2); // user2 is INCORRECTLY typed as Pick<User, keyof User>
console.log(user2.foo); // We do NOT get any error. This is bad!

However as you can see, with this simple change, the type safety of user2 is lost, and now I do not get any error when accessing user2.foo, which is really bad. How can I solve this? I have tried countless things for hours and haven't really found a working solution.

Edit: Code Sandbox with exactly the same code where Typescript errors can be seen and played with.

CodePudding user response:

const DEFAULT_USER_FIELDS: ('_id' | 'username')[] = ['_id', 'username'];

The reason getUserById ever works in the first place is because Field will be inferred to the union of the elements of fields, which will be more specific than just keyof User (Pick<User, keyof User> is just User). Without the function call to guide inference of the list's type you have to give that type yourself.

You could avoid repeating the fields by repeating the trick from getUserById instead:

function fieldsOf<T>(): <Field extends keyof T>(fields: Field[]) => Field[] {
  return fields => fields
}

const DEFAULT_USER_FIELDS = fieldsOf<User>()(['_id', 'username']);

DEFAULT_USER_FIELDS has the same type and value either way.

CodePudding user response:

Completely based on @HTNW's first solution, I found a way to slightly improve it so that I still have to repeat the field names but at least we have type safety in the definition of DEFAULT_USER_FIELDS itself:

const DEFAULT_USER_FIELDS: (keyof Pick<User, '_id' | 'username'>)[] = ['_id', 'username'];

But actually even that is still somewhat error prone because even though we cannot for example mistakenly write usernameee 2 times, the actual value of the array could be empty or have repeated values. After more thinking I think this is the verbose solution that would fully prevent inconsistencies between the declared compile-time types and the run-time values of the array:

const DEFAULT_USER_FIELDS: [
    keyof Pick<User, '_id'>,
    keyof Pick<User, 'username'>
  ] = ['_id', 'username'];
  • Related