TypeScript is wasting my time
Guillaume Martigny
Posted on April 22, 2022
⚠️ This is a bit of a rant as I've lost patience with TypeScript and I need to vent.
While converting a medium sized Nuxt application (~15 pages, i18n, auth, REST API) to TypeScript, I compiled a list of pain points (no specific order). This is not the first time that TS made me miserable while trying to use it. Maybe this is a "me" problem and I lack knowledge or skills. But, if this is the case, I bet that a lot of new developers also hit these roadblocks and didn't say anything because of the hype surrounding TS.
Is it null
tho ?
Consider this simple "cache initialization" code:
Object.keys(localStorage).forEach((key) => {
store.commit('cache/init', {
key,
value: JSON.parse(localStorage.getItem(key)),
});
});
It iterate over all items in localStorage
and send the parsed value to the store.
Here, I get an error under localStorage.getItem(key)
because JSON.parse
accept string
as its first argument and localStorage.getItem
can return null
. Ok ... but ... whatever dude ! JSON.parse(null)
doesn't even produce an error. Even worse, I can't have unset values because I'm looping over existing items in the localStorage
.
"compact"
is not "compact"
Consider this number formater code:
function formatNumber(value: number, lang: string = 'en') {
const options = {
notation: 'compact',
maximumFractionDigits: 1,
};
const formatter = Intl.NumberFormat(lang, options);
return formatter.format(value);
}
The options
parameter is underlined with an error because the field notation
is a string when it should be "compact" | "standard" | "scientific" | "engineering" | undefined
. Well ... it's hardcoded to "compact"
, which is pretty close to "compact"
to me.
Type IDontCare
Consider this plugin declaration in Nuxt:
export default (_, inject: Function) => {
inject('myPlugin', /* Plugin code */);
};
In Nuxt, plugins are called with 2 parameters. The first is the Nuxt context, the second is a function that add the plugin to said context.
Here, I don't use the context, so set it to _
(as per the recommendations). But I get an error because it has an implicit type any
. I mean ... right, but who cares ? I'm not using this parameter. I have specifically renamed it to inform that I don't use it. Why does it reports as an error ?
Code duplication !
This one is pretty nasty to me. Again, consider a plugin declaration in Nuxt. This plugin expose a set of function.
export default (_: DontCare, inject: Function) => {
const API = {
get(key: string): object { /* Code code code */ }
set(key: string, value: object): void { /* Code code code */ }
};
inject('myPlugin', API);
};
Everything's good until there. Now, I want to use it in my code. I have to declare the injected function in every possible place.
interface API {
get(key: string): object
set(key: string, value: object): void
}
declare module 'vue/types/vue' {
// this.$myPlugin inside Vue components
interface Vue {
$myPlugin: API
}
}
declare module '@nuxt/types' {
// nuxtContext.app.$myPlugin inside asyncData, fetch, plugins, middleware, nuxtServerInit
interface NuxtAppOptions {
$myPlugin: API
}
// nuxtContext.$myPlugin
interface Context {
$myPlugin: API
}
}
declare module 'vuex/types/index' {
// this.$myPlugin inside Vuex stores
interface Store<S> {
$myPlugin: API
}
}
export default (_: DontCare, inject: Function) => {
const API: API = {
get(key) { /* Code code code */ }
set(key, value) { /* Code code code */ }
};
inject('myPlugin', API);
};
The worst part is not even that I have to tell TS that Nuxt is injecting my plugin everywhere. The worst part is that I have to make sure that every function signature in the plugin match with the interface. Why can't I infer types from the API itself ? Also, ctrl + click
become useless as it points to the interface and not the implementation (maybe an IDE issue, but still ...).
The cherry on top is that now, ESlint is pouting because function params in the interface are considered unused.
Import without the extension
TS need the file extension to detect the file type and compile accordingly. Fair enough, but now I have to go through all my import and add .vue
everywhere.
Dynamic interface
I have an URL builder that I can chain call to append to a string.
const API = () => {
let url = 'api';
const builder = {
currentUser() {
return this.users('4321');
},
toString() {
return url;
}
};
['users', 'articles', /* ... */].forEach((entity) => {
builder[entity] = (id) => {
url += `/${entity}${id ? `/${id}` : ''}`;
return builder;
};
});
};
// Usage
const url = `${API().users('4321').articles()}`; // => 'api/users/4321/articles'
This is fine and dandy until TS coming shrieking. I can declare a type listing my entities and use this type as key in a Record
(see Code duplication !). But I also need to describe the toString
and currentUser
methods aside.
type Entities = 'users' | 'articles' | /* ... */;
type URLBuilder = Record<Entities, (id?: string) => URLBuilder> & {
currentUser(): URLBuilder
toString(): string
};
const API = () => {
let url = 'api';
const builder: URLBuilder = {
currentUser() {
return this.users('4321');
},
toString() {
return url;
}
};
const entities: Entities[] = ['users', 'articles'];
entities.forEach((entity) => {
builder[entity] = function (id?: string) {
url += `/${entity}${id ? `/${id}` : ''}`;
return this;
}
});
return builder;
};
Problem solved ? Not quite ... The temporary builder initialized while building the whole thing is not yet of type URLBuilder
. I have no idea how to say "This will be of type T in a few lines".
Conclusion
I'm absolutely sure that all those issues are due to some lack of knowledge. If you have an elegant solution for any of those, please share in the comments.
Microsoft is not investing so much energy in something that's wasting time. I would love to come back to this article in a few years and finding all of this ridiculous, but for now, I really don't get the hype around Typescript.
Thanks for indulging me 😎
Posted on April 22, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.