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

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:

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.