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.
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.