An Advanced Generic Constraint - ldco2016/microurb_web_framework GitHub Wiki

So we have our UserProps Type: Screen Shot 2020-12-01 at 2 10 27 PM

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: Screen Shot 2020-12-01 at 2 22 03 PM

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: Screen Shot 2020-12-01 at 2 10 27 PM

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: Screen Shot 2020-12-01 at 2 22 03 PM

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: Screen Shot 2020-12-01 at 2 22 03 PM

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.

⚠️ **GitHub.com Fallback** ⚠️