The Get Method's Shortcoming - ldco2016/microurb_web_framework GitHub Wiki

Currently, the return type signature of my get() method is not exactly appropriate:

export class Attributes<T> {
  constructor(private data: T) {}

  get(propName: string): number | string {
    return this.data[propName];
  }

  set(update: T): void {
    Object.assign(this.data, update);
  }
}

Right now it's either a number or a string I am returning.

I am going to imagine that I decide to put in a limitation inside this application and say we can only ever have attributes that are number | string | boolean like so:

export class Attributes<T> {
  constructor(private data: T) {}

  get(propName: string): number | string | boolean {
    return this.data[propName];
  }

  set(update: T): void {
    Object.assign(this.data, update);
  }
}

So I would have the basic types covered here and maybe I just run with this and say I can only ever store these types of values and my code would work just fine. Well, for the most part, if I went down this route, the following is the code I would have to write anytime I want to access the properties inside this get() method. Once we see this difficulty, we will understand why we want to get away from this union pattern I currently have.

I am going to create an instance of Attributes with UserProps serving as my type and I will look at how awkward it will be to make use of the current get() method like so:

import { UserProps } from './User';

export class Attributes<T> {
  constructor(private data: T) {}

  get(propName: string): number | string | boolean {
    return this.data[propName];
  }

  set(update: T): void {
    Object.assign(this.data, update);
  }
}

const attrs = new Attributes<UserProps>({ id: 5, name: 'asdf', age: 20 });

So now I will try to access id through attrs or specifically through the get() method. To do so, I would have to do the following:

import { UserProps } from './User';

export class Attributes<T> {
  constructor(private data: T) {}

  get(propName: string): number | string | boolean {
    return this.data[propName];
  }

  set(update: T): void {
    Object.assign(this.data, update);
  }
}

const attrs = new Attributes<UserProps>({ id: 5, name: 'asdf', age: 20 });

const id = attrs.get('id');

Here is the issue, if I now mouse over id, I get back a type annotation on id of string | number | boolean:

Screen Shot 2020-12-01 at 11 18 54 AM

And thats because inside the class of Attributes it says when I call get() I am going to return one of the possible types:

get(propName: string): number | string | boolean {
    return this.data[propName];
  }

So before I can access any property on id that only belong to a number, I have to set up one of those type guards. If I try to access any properties on id I will only see the properties that are common between number, string, boolean.

So to treat id as if it were truly a number, I have to setup a type guard like so:

if (typeof id === 'number') {
  id;
}

If I mouse over id, I see that now it is a number:

Screen Shot 2020-12-01 at 11 23 53 AM

So I would need to repeat this type guard every single time I needed to access a property off of my Attributes object. This is not a pattern I want to dive into. It would be better to just call attrs.get() and have whatever value I get back be of the correct type, that would be super ideal.

I want to be able to say this id here, if I call get() with that id I want it to be a number 100% of the time without any additional hassle on my side.

One way I could do this is to put on a direct type annotation like so:

const id: number = attrs.get('id');

if (typeof id === 'number') {
  id;
}

But if I do that I am going to see an error: Screen Shot 2020-12-01 at 11 29 36 AM

The error is saying I can only access one of these possible values and I cannot just assume that it is of type number, so that would not work out.

The other thing I could try doing is using a type assertion. A type assertion is where I try to override TypeScripts behavior and let it know that I know what I am doing and to believe me when I say that id is a number like so, const id = attrs.get('id') as number;.

So that right there is the type assertion I am trying to just override TypeScript and tell it whatever I get back from this function call, its going to be a number, just trust me.

So now when I mouse over id: Screen Shot 2020-12-01 at 12 01 18 PM

Yep, its a number, but obviously this approach has a downside. If I ever come back to my code and decide that instead of id I actually want to get my name, const name = attrs.get('name') as number;. Well, name is definitely not a number, so if I put out some code like this, TypeScript would think its okay, but when I try to run it, chances are something will not work as expected because I am treating name which is a string as though it were a number. So its clear that all these possible solutions would have, well, some really big downsides.

So how do I just call attrs.get('name') and have TypeScript understand what I am trying to do.

With just this code here, const name = attrs.get('name');, I need TypeScript to understand that name is 100% of the time is going to be a string. Now to make that happen I will have to use some complicated syntax around generics.

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