Why you should avoid primitives in TypeScript? - real use cases
02/01/2023
3 min read
Primitives are basic types available in TypeScript such as
number
string
or boolean
. We are using them very frequently defining our variables, objects’ properties, class fields etc. Although there are cases where primitives could make our life more difficult and we should use some more specific types.Last time I’ve been working on feature to track user action in app and send metrics to Elastic APM. In general, it should be tracked whether and which fields the user touched and/or changed their value. To difference fields we had to add some parameter. Lets say it’s
apmId
. Intuitively, you could assume that this id is a string, so the component looked more less like this:@Component({selector: 'form-first-field',template: `<form-field [apmId]="apmId"></form-field>`})export class FormFirstField {apmId: string = "firstField"}
At first glance it seems ok. There is component which pass
id
typed as string to his child. Nothing interesting. But... what if there are 10 or more such components?Try to look at this problem in other way. Case is: we want to find all components which are tracked by APM. Having
apmId
as string
we are unable to find them quickly. There is no relation between apmId
and his purpose. Someone can say: “why don't you search by apmId
phrase?”. I could but this field can have different name in other components and it’s searching by string not import reference. Another scenario: we need to use apmId
in other place in code. If we don't remember value, we need to search it throughout application. In this place there is very easy way to commit typo which will probably cost us hours of debugging.Well.. what is solution?
Create own type! In my case it was type which contains names of every possible fields which must be tracked (it doesn’t necessarily have to be every field on the form). So my type looks something like that:
type ApmTrackedField = 'firstField' | 'secondField' | 'someSpecialField' ...
And lets use it in our components:
//Component 1@Component({selector: 'form-first-field',template: `<form-field [apmId]="apmId"></form-field>`})export class FormFirstField {apmId: ApmTrackedField = "firstField"}//Component 2@Component({selector: 'form-secondField-field',template: `<form-field [apmId]="apmId"></form-field>`})export class FormsSecondField {apmId: ApmTrackedField = "secondField"}//other components
As you can see our
apmId
is now very specific type. It can easily be searched in IDE which components have something related with tracking user action feature. We are safe for the future when field name will change. TypeScript immediately throws an type error. Also in case of using apmId
in other place in code i.e.: as argument of some method, IDE provides possible values, so we are keeping away from typos.Real-life example shows how it’s important to have proper types. If we have very specific domain in project, we shouldn’t use primitives even if it looks like that:
type Price = number
It’s clear that
price
is number (or string) but what if new requirement arrives from business to differ price by gross/net or having it in USD and EURO. By this typing we are able to find all places which are using Price
and we can estimate how big could be to change. Having all prices as number
we cannot do it simple.