This content originally appeared on Bits and Pieces - Medium and was authored by James Marks
Just because you can do these, doesn’t mean you should.
Most of us are aware that in JavaScript, you can do many things you shouldn’t. Many people first starting with TypeScript and nearly as many who have been using TypeScript for a long time may not have considered that the same thing applies in TypeScript, too.
While TypeScript can be an effective tool to protect against human error and thoughtlessness or forgetfulness, it has to try to facilitate doing all things in JavaScript that people have always been able to do. Both the good and the bad.
On top of that, it needed some ways to ensure that they weren’t slowing down development in cases such as rapid prototyping or unit testing. Thus, it provides some features that allow developers to circumvent its otherwise powerful ability to save you from yourself.
These “escape hatches” that TypeScript allows you to use are sometimes related to things you can do in JavaScript with or without TypeScript, but you may not be aware of their detrimental effect on your projects.
Well written code is easy to understand and easy to change. Continuously seek to break down complexity into smaller simpler parts. Reduce tech-debt, increase code comprehension and maintainability.
1. Union Types
While you can’t control what data is being passed around outside of your own code, inside your code you can get more certain about your own data.
Union types are a means of allowing data to take many shapes, which sounds great in concept.
However, In practice, you usually end up dealing with each shape of that data differently.
In fact, what typically ends up happening is that you write overly complicated code, which becomes a difficult-to-maintain mess.
Don’t make one-size-fits-all functions or methods!
Instead of trying to write a clever function that does everything, write code that handles each case separately and clearly to gain a happier and more productive developer experience.
Avoid using union types. Be explicit and purposeful about how you handle data.
Here are some common ways that I’ve seen union types being used and how they can be improved.
Union types for function parameters
Sometimes you want to write a “catch-all” function that will take whatever you give it and do something different depending on what you’re given.
While this is not the most dangerous offender in this article, it is unhealthy for your code, and for most people, it doesn’t feel like a real problem until well after it is a big drain on resources to fix.
At some point in the future, you may want to diverge how you respond when given a particular type or two as input, perhaps providing a completely different type of return value.
The only way to make this change is to write a new function that accepts the single parameter type and provides a new return type and then sift through all calls to the original function to ensure that any using that particular parameter type are changed to call the new one. This is sticky and dangerous and can be difficult to guarantee is done right.
The better choice is simply to write multiple publicly exposed functions with distinct names and similar but differently typed parameters as input. You can delegate each of these to call your original function (which should be private) internally to ensure you adhere to DRY principles.
Union types for return values
This issue often goes hand-in-hand with union types for parameters, but might also be found on its own. Both cases come from similar sentiments. The developer, with the best of intentions, is merely trying to create a single function that somebody can use for many purposes.
Excluding `null` (T | null), if a function returns more than one data type as its response value, then you are likely overloading it with too much responsibility.
Returning multiple potential types from your function requires your consumer code to do a great deal more branching, increasing the complexity of your code everywhere that you use the offending multiple-returner function.
One function that returns multiple types causes additional complexity throughout your code, not only within itself.
In my experience, you can almost always split your single, high-complexity function into several multiple, simplified, functions. Give those functions good self-described names and now your consumers can use them in a way that is more obvious and specific.
Union types for variables
Whether or not you ascribe to the belief that you should avoid mutation in your code, there is no good reason to have the exact same variable potentially contain data from different sources that might be a string or a number, or an array.
Instead of this, use type-narrowing and type-guards to ensure that your source data is being delegated to small single-purpose functions meant to handle data of a distinct type.
This keeps your code well organized and reduces the instances of branching and sub-branching that might occur at the code-block level.
💡 Along with TypeScript you could also use an open-source toolchain like Bit. With Bit you can easily share, discover, and reuse individual components across different projects, which can significantly reduce code duplication and improve code quality. By breaking down complex code into smaller, reusable components, you can create more modular and maintainable codebases. Bit also integrates seamlessly with TypeScript, making it easy to share and manage typed components. Learn more about this here, here, and here.
Find out more:
Sharing Types Between Your Frontend and Backend Applications
2. Using `any`
TypeScript’s any is a necessary evil for TypeScript itself to satisfy the demands of the masses. It's useful for rapid prototyping, or just for getting the initial code started before taking some time to do things more correctly.
But in your production applications, it is highly damaging for you to use any. Ban it. Destroy it. Exorcise it from your commits and merge requests!
any is a tool that essentially escapes all of the type safety that TypeScript attempts to provide, making it so that the associated value can be of literally any type.
You can use it in any way you want, any time you want, often without regard for whether you successfully or specifically enforced typing via type guards.
In practice, when you decide to make something an any type, it is normally for one of two reasons:
Either, you don’t want to take the time to figure out how to do it right.
Or, you have some assumptions about the possible types that the value might hold, but you don’t have any way of knowing for sure what type it is beforehand.
In this case, instead of setting the type to any, use unknown instead and then rely on type narrowing and type guards to handle the use cases you intend to handle. Optionally include a fallback.
This most certainly takes more code, but your code becomes more robust and less prone to runtime error.
Whether you want to do something different for something that is a number type than you want to do if it is a string type, or you want to do the same thing for both string and number types, you can use features such as type guards and assertions to ensure that the correct code is executed for the correct data type.
3. Using `as`
When I first started using TypeScript I didn’t understand that there were more effective, more correct alternatives to using as to tell the TypeScript parser what type of data I received from an API call.
In general, a developer’s intention when using as is to validate that something is a particular shape and to hint to TypeScript about what that shape is.
In reality, as silently does more than provide a hint. It can also coerce data to look like a given type and only provide a partial match.
It can effectively lie about your data, leading to cascading logic errors and difficult-to-track run-time bugs.
Because of as's ability to lie to TypeScript, there are circumstances even with a seemingly innocent .reduce where this might cause damage. And when you do end up with a runtime error as a result, it is often very difficult to trace.
Here’s an example:
const response = await fetch('http://someendpoint.com/api/thing/1');
if (response.status === 200) {
const responseData = await response.json() as TMyType;
}
Responsible code is pessimistic and does not trust I/O from external sources under any circumstances.
Even if it was you who created that API endpoint and you feel 100% certain that you’ll always provide the correct response type, don’t trust it. Some day, you or some other developer will return and change that code — and your consuming application should be prepared to handle the possibility that a mistake will be made.
On top of that internet data transfer protocols always have a small chance of error, internet connections go out, and so on. You can not count on outside input being correct 100% of the time.
When you receive an API response, you should be using type-guards and/or type assertions to ensure that the data you received.
// For this example, TMyType must have a property 'foo' that is a string.
type TMyType = {
foo: string;
};
const assertMyType = (data: unknown): TMyType => {
if (
data
&& typeof data === 'object'
&& 'foo' in data
&& typeof data.foo === 'string'
) {
return data as TMyType;
}
throw new Error('TMyType Assertion failed.');
};
const doAThing = async () => {
const response = await fetch('http://someendpoint.com/api/thing/1');
if (response.status === 200) {
const responseData = assertMyType(await response.json());
doSomethingWithResponse(responseData);
}
};
Firstly, you’ll notice I still used as here. Unfortunately, this is a strange quirk with TypeScript.
If you maintain the rule that the only time you’ll use as is inside the context of an assertion function, it will serve you well. Also, just be certain to validate that the function does as intended. Use unit testing whenever you can.
Yes. This is definitely a little more work than the original example, particularly if you consider that most type assertions will need more than one property to be validated (consider a validation framework).
However, it is important to keep in mind that this is work that should be done with or without TypeScript if you want a complete, professional, self-documenting, and secure application.
TypeScript doesn’t cause the need for the added work, it simply exposes and enforces the need.
Another common use case for `as`
Another way that I used to use as is to lazily add types to the result in the Array prototype function `reduce`'s second argument.
const fruitSalad = fruits.reduce((acc, current) => {
return [...acc, current.name];
}, [] as string[]);
.map, .reduce, and .filter among countless other functions use generics and TypeScript’s built-in type inferencing to implement meaningful type restrictions on code.
When inferencing, TypeScript does its best to narrow down to the most likely possible type with the information it has, and that means it isn’t always what you intend. In the above, had I not provided a hint that the array was an array of strings, TypeScript would have considered it to be a never[] .
Don’t do as I did. Provide the type using the type generic system in angle brackets <> .
const fruitSalad = fruits.reduce<string[]>((acc, current) => {
return [...acc, current.name];
}, []);
This avoids the small, but all too human chance of accidentally using as to enforce an incorrect type.
4. Using the Definite Assertion Operator: `!`
TypeScript wants to help by ensuring you are protected against missing data. It analyzes your code, tells you that there is a chance that you might be missing data, and then you go ahead and say “I promise the value is there”.
Why, oh why would you do this to yourself?
// Person has a .middleName property that is optional, so it might be `undefined`
const middleInitial = (person.middleName!)[0];
It takes a few moments more to use a type guard and provide some logic to handle a case where the value doesn’t actually exist, no matter how much of a longshot that is.
You may choose to put up an alert, throw an error or something else. It's up to you, how you want to respond.
if (person.middleName === undefined) {
// Your choice of user feedback.
return;
}
const middleInitial = (
person.middleName
? person.middleName[0]
: ''
);
Doing this creates code that doesn’t just claim that something exists. You have effectively proved to TypeScript that you have a defined value.
Just make sure that the code after the if block doesn’t execute when your value is undefined. An early return is a common way to handle this, but throwing an Error is another option. I’m sure you can find others.
5. Writing Unit Tests Restricted by TypeScript Rules
TypeScript is WONDERFUL, but it is not perfect. It acts as a deterrent to issues with data types, but there is no 100% guarantee that all of the types it will assume are true at compile time will continue to be true at run time.
If you write your tests using TypeScript — and particularly if you write using a disciplined set of style rules such as those described above — the rules of TypeScript may keep you from writing effective tests.
Write your tests in plain JavaScript OR break the rules above and use as or any to bypass type restrictions in your unit tests in order to write failing code.
For unit testing to be fully effective you must write failing tests as well as successful ones. That way when somebody makes a change to your tested code later, it will be much more difficult for them to break.
Get Coding!
I hope you found this article helpful, useful, and interesting!
Keep an eye out for my next few articles where we’ll explore alternative strategies to other common paradigms.
My goal is to make the world’s code more beautiful, useful, and effective, one developer at a time.
I’m always excited to share more ways to improve your code and make it more maintainable, readable, and productive.
Happy coding!
Check out one of my other articles below:
- These Alternatives to `if` Will Make Your Code 100x Better
- Why Typescript is Better than JavaScript for Team Projects
Build Apps with reusable components, just like Lego
Bit’s open-source tool help 250,000+ devs to build apps with components.
Turn any UI, feature, or page into a reusable component — and share it across your applications. It’s easier to collaborate and build faster.
Split apps into components to make app development easier, and enjoy the best experience for the workflows you want:
→ Micro-Frontends
→ Design System
→ Code-Sharing and reuse
→ Monorepo
Learn more:
- How We Build Micro Frontends
- How we Build a Component Design System
- How to reuse React components across your projects
- 5 Ways to Build a React Monorepo
- How to Create a Composable React App with Bit
5 Things You Should Avoid Doing with TypeScript was originally published in Bits and Pieces on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Bits and Pieces - Medium and was authored by James Marks
James Marks | Sciencx (2023-03-26T06:56:52+00:00) 5 Things You Should Avoid Doing with TypeScript. Retrieved from https://www.scien.cx/2023/03/26/5-things-you-should-avoid-doing-with-typescript/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.