TypeScript: The Intellectual Masturbation of Web Dev?
Posted on March 15, 2024 - by Andy Cinquin
TypeScript vs JavaScriptAdvanced web developmentCode complexityStatic and dynamic typesOver-engineering in developmentTypeScript for projectsTypeScript code examplesUI libraries for front-end developmentUnit testing and E2EAutocompletion and modern IDEsPerformance of development environmentsSimplified web developmentError management and code qualityModern development technologiesZod for type validation
Introduction:
"Why is everyone talking about TypeScript like it's the web dev revolution?" Well, let me tell you that behind the buzz, there's a lot of "intellectual masturbation" going on around TypeScript. It's a bit of a crude term, I know, but it captures the essence of this phenomenon where we complexify simple things for the pleasure of feeling smarter. Let's break it down together.
Part for beginners
What is TypeScript anyway?
TypeScript is a "superset" of JavaScript. But what does that mean? Think of JavaScript as a basic pizza - cheese and tomato. TypeScript is the same pizza, but with tons more ingredients. On paper, it sounds great, but sometimes you just want a simple pizza that does the job.
Why do some people say TypeScript is the best?
TypeScript's big promise is to add "types" to JavaScript. It's supposed to help you avoid errors by forcing you to define in advance the kind of data (number, text, etc.) you're manipulating. Sounds useful, especially when you're working on big projects.
But then, what's the big deal?
The big deal is that this extra layer adds quite a bit of complexity. For a simple or medium-sized project, it can turn a leisurely stroll into an obstacle course.
Example for beginners
Let's say you just want to add two numbers in JavaScript:
function add(a, b) {
return a + b
}
console.log(add(5, 3)) // 8
Simple, isn't it? Now here's what it would look like in TypeScript :
function add(a: number, b: number): number {
return a + b;
}
console.log(add(5, 3)); // Result: 8
We need to specify that
a
and b
arenumbers
. For this simple piece of code, it's still fine, but imagine on a whole project!Part for the experts
TypeScript's over-engineering
For the experts among you, TypeScript's appeal may lie in its ability to model complex types and provide an advanced type system. However, with this sophistication comes complexity. In many cases, this complexity doesn't add value commensurate with the time invested.
Extreme cases where TypeScript complicates more than it helps
Let's take the example of modeling an extremely heterogeneous API response. In TypeScript, you could end up defining dozens of interfaces to handle every possible case. This quickly becomes a gas factory, especially if the API changes frequently.
Example for the experts
Let's imagine that you need to manage a complex API response. In TypeScript, you might write something like :
interface ApiResponse {
userId: number;
id: number;
title: string;
completed: boolean;
}
function fetchTodo(): Promise<ApiResponse> {
// Simulation d'un appel API
return fetch('https://example.com/todos/1').then(response => response.json());
}
It's accurate, but take a look at the JavaScript version :
async function fetchTodo() {
const response = await fetch('https://example.com/todos/1')
return await response.json()
}
In JavaScript, we get straight to the point. We fetch the response, and trust our knowledge of the context to handle the data correctly.
Conclusion:
So, is TypeScript genius or intellectual masturbation? The truth, as is often the case, lies somewhere in the middle. For large projects with many hands in the code, TypeScript can be a valuable tool. But for many other situations, it adds a layer of complexity that can unnecessarily slow down and complicate development.
Let's remember that, at the end of the day, the goal is to create applications that are functional and enjoyable for the user. Simplicity often has its own form of elegance and efficiency. And sometimes, a good old-fashioned cheese and tomato pizza is just what you need.
The IDE Improvement Argument: A relic of the past?
An argument often put forward in favor of TypeScript is its positive impact on the performance of integrated development environments (IDEs): indeed, thanks to its typological rigor, it facilitates autocompletion and potentially improves developer productivity. However, with the advent of artificial intelligence technologies and the considerable progress made in modern IDEs, this advantage is tending to become obsolete.
Today's IDEs, armed with advanced algorithms and AI, are capable of providing extremely precise suggestions and autocompletions, even in dynamic languages like JavaScript. This technological evolution calls into question the need for a strict typing system like the one proposed by TypeScript, particularly for projects that don't justify its added complexity.
In fact, the marginal gain in productivity offered by TypeScript's autocompletion features could be outweighed by the time invested in defining and maintaining complex type structures. In the age of AI, where development tools are evolving by leaps and bounds, JavaScript's simplicity and flexibility are regaining their place at the heart of developer efficiency.
Towards a reconciliation of approaches
It's not so much TypeScript itself that's in question, but rather how it's used. For large-scale projects requiring high levels of maintainability and scalability, TypeScript has its place. Yet it's crucial to recognize that, in an ever-changing technological world, yesterday's tools are not necessarily suited to tomorrow's challenges.
The key lies in a critical, contextual assessment of TypeScript's usefulness for each project. As with our pizza, it's a question of choosing ingredients according to the desired taste, not out of habit or under the pressure of trends.
Let's remember that the ultimate goal is to build applications that delight users, and this path can be as varied as the developers who take it. Sometimes, embracing the simplicity and agility of native JavaScript is exactly what it takes to cook up something delicious.
Here's one last extreme example, with the use of zod on top for the road! Here's a piece of code that handles file generation in an application: in js :
import { generateData } from '@api/utilityAPI';
import { CustomModal } from '@components/Utility/UtilityModal/CustomModal';
import { Button, CircularProgress } from '@mui/material';
import { handleError } from '@utils/errors/handleCustomError';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import styles from './UtilityModule.module.scss';
const DataGeneration = ({ id }) => {
const { t } = useTranslation();
const [invalidData, setInvalidData] = useState(null);
const [isPanelOpen, setIsPanelOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const handleAction = () => {
setIsLoading(true);
generateData(id)
.then((response) => {
FileUtil.download(response.blob, response.fileName);
})
.catch(async (error) => {
let invalidDataTemp;
if (error.response.data.type === 'application/problem+json') {
const jsonError = JSON.parse(await error.response.data.text());
if (jsonError.title === 'INVALID_DATA') {
invalidDataTemp = jsonError.invalidData; // Directly using the parsed JSON
}
}
if (invalidDataTemp) {
setInvalidData(invalidDataTemp);
setIsPanelOpen(true);
} else {
handleError(error);
}
})
.finally(() => setIsLoading(false));
};
return (
<>
<Button
variant="contained"
onClick={handleAction}
disabled={isLoading}
className={styles.actionButton}
>
{isLoading && <CircularProgress size="1rem" />}
{t('utility.actionButton')}
</Button>
{invalidData && (
<CustomModal isOpen={isPanelOpen}
onClose={() => setIsPanelOpen(false)}
invalidData={invalidData}
/> )}
</>
);
};
export default DataGeneration;
And here in typescript :
import { generateData } from '@api/utilityAPI';
import {
AnonymizedDataSchema,
TAnonymizedDataResponse
} from '@appTypes/utility/response/AnonymizedResponse';
import { CustomModal } from '@components/Utility/UtilityModal/CustomModal';
import { Button, CircularProgress } from '@mui/material';
import { FileUtil } from '@utils/UtilityFiles';
import handleError from '@utils/errors/handleCustomError';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import styles from './UtilityModule.module.scss';
type ActionProps = {
id: number;
};
export const DataGeneration = ({ id }: ActionProps) => {
const { t } = useTranslation();
const [invalidData, setInvalidData] = useState<TAnonymizedDataResponse | null>(null);
const [isPanelOpen, setIsPanelOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const handleAction = () => {
setIsLoading(true);
generateData(id)
.then((response) => {
FileUtil.download(response.blob, response.fileName);
})
.catch(async (error) => {
let invalidDataTemp;
if (error.response.data.type === 'application/problem+json') {
const jsonError = JSON.parse(await error.response.data.text());
if (jsonError.title === 'INVALID_DATA') {
invalidDataTemp = AnonymizedDataSchema.parse(jsonError.invalidData);
}
}
if (invalidDataTemp) {
setInvalidData(invalidDataTemp);
setIsPanelOpen(true);
} else {
handleError(error);
}
})
.finally(() => setIsLoading(false));
};
return (
<>
<Button
variant="contained"
onClick={handleAction}
disabled={isLoading}
className={styles.actionButton}
>
{isLoading && <CircularProgress size="1rem" />}
{t('utility.actionButton')}
</Button>
{invalidData && (
<CustomModal isOpen={isPanelOpen}
onClose={() => setIsPanelOpen(false)}
invalidData={invalidData}
/> )}
</>
);
};
----
import { z } from 'zod';
export const AnonymizedDocumentSchema = z.object({
documentFlag: z.boolean(),
optionalIds: z.array(z.number()).optional().nullable(),
});
export const PreEvaluationDataSchema = z.object({
sourceFlag: z.boolean(),
dobFlag: z.boolean(),
birthPlaceFlag: z.boolean(),
citizenshipFlag: z.boolean(),
emailFlag: z.boolean(),
idNumberFlag: z.boolean(),
certFlag: z.boolean(),
institutionFlag: z.boolean(),
studyLevelFlag: z.boolean(),
studyAreaFlag: z.boolean(),
entryDateFlag: z.boolean(),
desiredStartDateFlag: z.boolean(),
agentNameFlag: z.boolean(),
});
export const ProposalEvaluationSchema = z.object({
proposalDateFlag: z.boolean(),
authorFullNameFlag: z.boolean(),
answerDateFlag: z.boolean(),
});
export const InterviewDataSchema = z.object({
contactFullNameFlag: z.boolean(),
meetingDateFlag: z.boolean(),
summaryFlag: z.boolean(),
prosFlag: z.boolean(),
consFlag: z.boolean(),
meetingPlaceFlag: z.boolean(),
});
// Using TypeScript to infer the types from Zod schemas
export type TAnonymizedDocument = z.infer<typeof AnonymizedDocumentSchema>;
export type TPreEvaluationData = z.infer<typeof PreEvaluationDataSchema>;
export type TProposalEvaluation = z.infer<typeof ProposalEvaluationSchema>;
export type TInterviewData = z.infer<typeof InterviewDataSchema>;
// Merging schemas for a comprehensive response structure
export const ComprehensiveEvaluationSchema = AnonymizedDocumentSchema.merge(
PreEvaluationDataSchema.merge(
ProposalEvaluationSchema.merge(
z.object({
docTypeValidityFlag: z.boolean(),
minimumInterviewsFlag: z.boolean(),
interviewFeedbackMap: z.record(z.string(), InterviewDataSchema),
})
)
)
);
export type TComprehensiveEvaluation = z.infer<typeof ComprehensiveEvaluationSchema>;
You be the judge, which version is better? Wouldn't comments be enough? If our api contracts are well controlled in the back-end?
Thank you for your visit, feel free to contact me for
any information, quote or collaboration proposal. I will
be happy to answer you as soon as possible.
Did you like this article? Feel free to share it!