6 min read
Making the most of TypeScript: Enhancing safety and productivity
How to master TypeScript, ensuring safety and speed are never compromised. Let's unlock the true potential of this remarkable language and revolutionize your development experience.
TypeScript has become my passion because it significantly accelerates my coding process, allowing for faster iteration and minimizing potential bugs. Nevertheless, I’ve observed that some beginners and developers do not share the same positive experience (including me when I was in the beginning of my journey). It’s unfortunate to see some developers using TypeScript in ways that hinder their productivity instead of enhancing it.
I believe TypeScript should never become a roadblock, rather, it should always be a tool that enables developers to excel in any situation.
Complexity of Typescript
Have you ever been in a situation where you needed a type from a Promise? Here is an example of how the helper can look like:
// helper
export type Unwrap<T> = T extends Promise<infer U>
? U
: T extends (...args: any) => Promise<infer U>
? U
: T extends (...args: any) => infer U
? U
: T;
// usage
const res = Unwrap<ReturnType<typeof fetchTodos>> = ...
Scary, right?
Or maybe you have encountered situations where libraries magically organize property types, preventing mistakes during library setup?
It’s important to note that TypeScript is not solely about these advanced features. In reality, most of the time, you won’t be dealing with such complex tasks, and that’s perfectly fine.
If you are not a library maintainer, your experience with TypeScript will likely involve writing JavaScript most of the time. In this scenario, it’s more accurate to view TypeScript as a powerful linter rather than treating it as an entirely separate language and learning it as such.
Conclusion: If it’s possible write as few types as possible, allowing the inference system to effectively determine and convey the code’s meaning. If you are new in Typescript, don’t learn it in the way you learn other languages, but try to add it to your project and consider it as a “linter”.
How to write less TypeScript?
Ideally, the majority of type definitions in your codebase should consist of parameters for functions, while everything else is automatically inferred from the returned values.
The ideal entry point for applying this approach is where the data returned from the server. The inference system can then propagate these types seamlessly to our application components.
Example with react-query
:
// useLicenseLimits.ts
import { useQuery } from 'react-query';
import axios from 'axios';
interface LicenseLimits {
todosCount: number;
todosPerHour: number;
}
export default function useLicenseLimits() {
return useQuery(['license-limits'], () =>
apiHttpService
.get<LicenseLimits>('/api-server/license-limits')
.then((res) => res.data),
);
}
// usage in component
const SomeComponent: FC<IProps> = () => {
const { data: licenseLimits } = useLicenseLimits();
// ^^^^^^^^^^^^^ of type LicenseLimits
return <>...</>;
};
On the other hand this is often the weakest place, as we tend to manually replicate the backend schema and assume that the data will be of a particular type. Let’s see what we can do here.
Increase Typescript safety with Zod
With zod, you can create reusable validation schemas and use them to validate data received from various sources, such as user inputs, API responses, or configuration files. It offers a declarative and intuitive API that simplifies the process of data validation and helps catch potential errors early in the development process.
Let’s rewrite the previous example with zod and see all the power of inference:
// useLicenseLimits.ts
import { useQuery } from 'react-query';
import axios from 'axios';
import { z } from 'zod';
const LicenseLimitsSchema = z.object({
todosCount: z.number(),
todosPerHour: z.number(),
});
export default function useLicenseLimits() {
return useQuery(
['license-limits'],
() =>
apiHttpService
.get<LicenseLimits>('/api-server/license-limits')
.then((res) => LicenseLimitsSchema.parse(res.data)),
// ^^^^^
// parse will validate the backend data and infers data schema here
// if validation failed parse will throw an error
);
}
// usage in component
const SomeComponent: FC<IProps> = () => {
const { data: licenseLimits } = useLicenseLimits();
// ^^^^^^^^^^^^^
// of type { todosCount: number, todosPerHour: number}
return <>...</>;
};
By using zod, we can ensure that the incoming data to our application will always match the expected structure.
Increasing development speed
I began my TypeScript journey with a Create React App project, where the configuration is set to prevent you from seeing any result if there are TypeScript errors.
And this is something that has slowed me down on numerous occasions, particularly when I’ve already written TypeScript definitions, only to discover they don’t work as expected, forcing me to attempt a different approach.
This was actually the main reason why I decided to migrate to Vite, where you can configure it in a way that TypeScript errors don’t prevent you from seeing your results.
Conclusion
In conclusion, when embarking on your TypeScript learning journey, consider incorporating it into your JavaScript projects as a powerful “linter” rather than treating it as a separate language.
By minimizing its usage to specific areas, you can leverage the full potential of TypeScript’s inference system.
Try to to maximize productivity and avoid unnecessary slowdowns, ensuring a seamless and efficient development experience.
Embrace TypeScript as a valuable tool to enhance your codebase while embracing its benefits without feeling overwhelmed by its complexities. Happy coding!