Always Prefer Type with a Narrower Scope in Typescript
Learn why you should always prefer types with narrower scopes such as Literal Types in Typescript
In this issue, we will be looking as to why you should prefer types with a more narrow scope in Typescript as compared to the standard types (strings, numbers, etc), that you get out of the box.
📢 The TSCongress 2023 edition is finally here!
Mark your calendars for September 21 & 22 to connect with the vibrant #TypeScript community and learn about everything from tooling to testing.
20+ industry experts including Mark Erikson, Orta Therox, Travis Fischer
5K devs gathering in the cloud
And here's a small hint: a 10% discount is right here by the link.
One of the best mental models to form about Typescript is thinking about types as a set of values. This allows you to form a mental picture of what a variable of a certain type can be assigned as values and also makes it easier to annotate type for values.
For instance, if we say a type is a String, then we know it could be as little as an empty string, a single character, or the whole bible, the same applies to other types as well, if a number, it’s any from zero to infinity or negative infinity, but must be a number.
And to my least favorite type - any
- this could be literally anything that can exist and can be assigned to any type. And I strongly discourage its use because it has superpowers, such as being assigned to any variable regardless of its type and vice versa. As you can imagine, this can introduce bugs quite easily, as any type prevents typescript from doing its important job of Type checking.
“An any
type is “contagious” in that it can spread through the codebase”
Effective Typescript
(Dan Vanderkam)
In this case, a string or number type is narrower compared to the any
type. But let’s take this a little further, and look at strings. If you annotated a variable, a type of string, as we had seen, it means the value could be anywhere from an empty string to a whole book.
For some variables, say an article content, this may be appropriate, but for others, this may be too wide, covering way too many values that we would really like. For instance, take a variable, with a limited number of options, like Tailwinds CSS screen possible screen sizes breaking points - xs
, sm
, md
, lg
, and xl
.
One possible way to annotate such a variable is:
let screenSize: string;
This works, as far as our options above are considered, as they are a subtype of strings.
// works
screenSize = "sm";
screenSize = "md";
screenSize = "lg";
The problem is that, now, on top of our option, our screenSize
variable can accept other variables, as long as they are string. And this is not our desired behavior.
screenSize = "Hello World"
// No errror
As you can probably imagine, this runs the possibility of introducing a bug within our application.
So, how can we improve this?
A more preferred version of the type for the screenSize
variable is a string literal union type, with the type being our five options, as shown below:
let screenSize: "xs" | "sm" | "md" | "lg" | "xl";
And just like before, this works for our usual values:
// still works
screenSize = "sm";
screenSize = "md";
screenSize = "lg";
But fails, if we assign a value that’s not in our list, Typescript is going to throw an error:
And now, we have a more type-safe application in our hands, compared to before.
This doesn’t just apply to strings alone, it could be numbers or other types, where we can use number literal types to ensure we provide types that are narrower in scope than numbers.
What about the any
type?
In some cases, you don’t really care about the type, for one reason or the other, and in such cases, we are usually tempted to use the any
type. But precision doesn’t hurt that much, so if you don’t care about the type of variable, but expect it to be an array, instead of just using any for the variable type, you can use the array of any - any[]
, which is more precise compared to the plain any.
The same applies to functions, instead of using any in place of a function, you can type the function, with arguments of any type and returns any, which is more precise.
type fn = (...fn: any []) => any
The same applies to objects as well, instead of a plain any, say we know it’s an object, whose keys are string or numbers but the value could be well anything, we can avoid using any, and instead create an object whose keys could be any.
type obj2 = {
[key: string | number]: any
}
// OR use the Record utility type in Typescript
type obj = Record<string | number, any>
Why? The question you need to ask yourself before using the plain any type is whether all values that exist in Javascript, from string to arrays, to objects, classes, and functions are acceptable for that variable. If not, prefer a more precise version of the same.
Still, my advice stands, use the
any
type sparingly, if you must.
Conclusion
In this issue of All Things Typescript, we looked at why you should consider using more narrower types of the standard types in Typescript to increase Types safety for your application. The same applies for the any
type, if you must use the any
type, prefer a more precise version of the same as compared to the plain any
type. While the any
type is not recommended, there are times when we simply don’t care about the type, but it doesn’t hurt lazily to ensure the type is more precise.