ref – https://stackoverflow.com/questions/57337598/in-typescript-what-do-extends-keyof-and-in-keyof-mean
Initially methods that return unions such as “string | number” is very limited.
Thus, let’s get away from union types.
Let’ take a look at this example. We create a class Attributes, which will will be initialized with interface type T.
1 2 3 4 5 |
import { UserProps } from './user' export class Attributes<T> { constructor(private data: T) {} } |
We initialize a sample interface called UserProps.
1 2 3 4 5 |
export interface UserProps { id?: number; name?: string; age?: number; } |
Thus, we’ll be passing UserProps into Attributes like so:
1 2 3 4 5 |
const attr = new Attributes<UserProps>({ id: 5, age: 20, name: 'whatever' }); |
This means Attributes will be type checking to make sure what we pass into constructor will conform to UserProps.
Now let’s implement get.
1 2 3 4 5 6 7 8 9 |
export class Attributes<T> { constructor(private data: T) {} get = <K extends keyof T>(key: K): T[K] => { console.log(`Attribute - getting key ${key}`) return this.data[key]; }; } |
keyof T
keyof T is the union of public property names of T.
In our example, we will be using UserProps as type T.
So “keyof T” will produce “name” | “age” | “id”.
1 2 3 4 5 6 7 8 9 10 11 12 |
export interface UserProps { id?: number; // declare id of type number name?: string; age?: number; } type UserPropsKeys = keyof UserProps; const a: UserPropsKeys = "id" // ok const b: UserPropsKeys = 2434 // not ok const c: UserPropsKeys = "name" // ok const d: UserPropsKeys = true // not ok |
K extends keyof T
1 |
get = <K extends keyof T>(key: K) |
K extends keyof T is used to constrain our function get function’s param key.
What we’re saying here is that since we’ll be passing in UserProps, “keyof T” will translated to “keyof UserProps”, which then return to us a union of “name” | “age” | “id”.
Then type K of param key must be one of these three. In other words, K can only be “name”, “age”, or “id”.
Note that the “extends” used here has nothing to do inheritance!
T[K]
T[K] is the return type.
Say if we were to pass in “name” into function get,
K will be “name”
T is UserProps,
Then T[K], UserProps[“name] will give the type string.
Now that the code is clear, let’s look at the full source and see how it is used:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import { UserProps } from './user' export class Attributes<T> { constructor(private data: T) {} get = <K extends keyof T>(key: K): T[K] => { console.log(`Attribute - getting key ${key}`) return this.data[key]; }; set(update: T): void { Object.assign(this.data, update); } } |
We first instantiate an Attributes where it accepts interface UserProps as T. The object we pass in must conform to UserProps with properties name, age, and id.
1 2 3 4 5 |
const attr = new Attributes<UserProps>({ id: 5, age: 20, name: 'whatever' }); |
We then use the instance and call function get with parameter name.
name conforms to keyof UserProps, so K is now name.
1 |
const name = attr.get('name') // name will have type string |
Since K is name then T[K] will be string.
We see this by hovering over name and intellisense will show the type string.
For age and id, its returned instances will have type number.
1 2 |
const age = attr.get('age') // age will have type number const id = attr.get('id') // id will be have type number |
This is a much better way than to return limited unions.