An Advanced Generic Constraint - ldco2016/microurb_web_framework GitHub Wiki
So we have our UserProps Type:
UserProps Type is an interface and an interface describes the structure of an object. In the case of UserProps
I am saying there are going to be three key/value pairs in an object that satisfies this interface.
There is a key of name
that is going to have a type of string
, a key of age
that has a type of number
and a key of id
that has a type of number
as well.
Here is what the ultimate goal is from the get()
method:
I essentially want to say that whenever I call get()
I want to essentially pass in "name"
and then ideally what I want TypeScript to do is use this string here as a lookup inside of that interface. So I really want TypeScript to take that string, look up that string inside of UserProps
interface and then understand that the key of name
corresponds to a string
.
So I want TypeScript to somehow understand that whenever I call get()
with a string
of name
, I will return a string
and something similar with age
and id
but with a number
.
So essentially I want to have a close relationship between this variable of "name"
and whatever gets returned. With that in mind I will update my get()
method like so:
export class Attributes<T> {
constructor(private data: T) {}
get(key: string): number | string | boolean {
return this.data[key];
}
set(update: T): void {
Object.assign(this.data, update);
}
}
So far nothing major I just changed over to key
to be more semantic. It makes understanding some of the syntax I am about to put in there a bit easier to understand.
So here is where the real refactor starts. I can use generics with method definitions like so:
export class Attributes<T> {
constructor(private data: T) {}
get<K extends keyof T>(key: K): T[K] {
return this.data[key];
}
set(update: T): void {
Object.assign(this.data, update);
}
}
We need to understand that the letter K
is not some special operator. Just like T
, it is meant to represent some kind of type. I can use any identifier I want in place of K
, but usually, by convention, we refer to this as K
.
With that in mind I will break this down. The <K extends keyof T>
sets up a generic constraint. The constraints limit the type that K
can be. I used this back inside my src/Sync.ts
file:
export class Sync<T extends HasId> {
constructor(public rootUrl: string) {}
fetch(id: number): AxiosPromise {
return axios.get(`${this.rootUrl}/${"id"}`);
}
.........
}
This setup a constraint that limited the types that T
could be and so I am doing the same exact thing with K
inside of Attributes.ts
file:
export class Attributes<T> {
constructor(private data: T) {}
get<K extends keyof T>(key: K): T[K] {
return this.data[key];
}
set(update: T): void {
Object.assign(this.data, update);
}
}
So K
can only ever be one of the keys of T
. In total, that step is saying that the type of K
can only ever be one of the different keys of T
. If I pass in a T
next to class of Attributes
with a type, it can only ever be a name
that is a string
, an age
that is a number
and an id
that is a number
:
So now inside of my argument, I am saying that whatever argument I am passing in, its going to be of type K
and because K
could only ever be of different keys of T
, which is my case right now its only name
,age
or id
:
And so I can only ever call get()
with name
, age
or id
, that's it. I can treat strings as though they were types. So between this first constraint: <K extends keyof T>
and the type of K
, I am saying I can only ever call get()
with name
,age
,id
as strings.
And then finally, the return type annotation is essentially the same as a normal object lookup. Whenever we have an object like const colors = { red: 'red' };
, we can look up something in there like so, colors['red'];
, that is a lookup.
So I am doing the same thing but with a type. I am saying look at the interface of T
which in my case I am imagining is only going to be UserProps, so look up that interface T
and then return the value at the key of K
and I have already established that it can only be name
, age
and id
. So TypeScript is going to look up name
, age
or id
and its going to return the corresponding type which will be string
or number
. That's it.
Let's Recap
The first part of the get()
method <K extends keyof T>
is saying that K
can only be one of the keys of T
. T
is going to be a sort of variable, so its not only ever going to be just UserProps, but right now the only thing I have to use inside this class is a type of UserProps:
So with this constraint I am saying that K
can only ever be name
, age
or id
.
I then try to make use of that type right away inside this argument: (key: K)
. I am saying key
of type K
can only ever be name,
age, and
id. Then I am saying take that type
T, whatever I passed in when I am referencing attributes and look up the type
Kinside there:
T[K]`.
So with all that in mind, I will try to write some code to actually test this thing out like so:
import { UserProps } from './User';
export class Attributes<T> {
constructor(private data: T) {}
get<K extends keyof T>(key: K): T[K] {
return this.data[key];
}
set(update: T): void {
Object.assign(this.data, update);
}
}
const attrs = new Attributes<UserProps>({
id: 5,
age: 20,
name: 'asdf'
});
const name = attrs.get('name');
I can use the get()
method and get back some variable that has the correct annotation type on it. Likewise for attrs
:
import { UserProps } from './User';
export class Attributes<T> {
constructor(private data: T) {}
get<K extends keyof T>(key: K): T[K] {
return this.data[key];
}
set(update: T): void {
Object.assign(this.data, update);
}
}
const attrs = new Attributes<UserProps>({
id: 5,
age: 20,
name: 'asdf'
});
const name = attrs.get('name');
const age = attrs.get('age');
const id = attrs.get('id');
I was able to get the above behavior because of this syntax: get<K extends keyof T>(key: K): T[K]
.
This is how I got attributes to work in a way I expect. I am setting up the constraint, limiting the different type that I can pass in as the argument then looking up the given key on the interface of T
to understand what type of value I am returning.