TypeScript is wasting my time
Tags: typescript, opinion, discuss, letmelivemylife
Published: 2022-04-22 12:47:46

⚠️ 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:

```ts
Object.keys(localStorage).forEach((key) => {
  store.commit('cache/init', {
    key,
    value: JSON.parse(localStorage.getItem(key)),
  });
});
```

[Playground](https://www.typescriptlang.org/play?target=5&jsx=0#code/PIIwVgpgxgLgdAawgTwM4AoA2B7KBDTAZRmwCc8BzCASjgDMyBRPKAC3XSWWoAIBeAHw8A3gCgePVCVIQ4UbAFsFASxjoA5PjYQA9MoB2q9QBoR4iTy7HzEgG4EArhABcPAFKFgAOTgAHPKSoEFi4BMRklLJUMACSMBAKnCjU1NYSAL7UANyimVlAA)

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:

```ts
function formatNumber(value: number, lang: string = 'en') {
  const options = {
    notation: 'compact',
    maximumFractionDigits: 1,
  };
  const formatter = Intl.NumberFormat(lang, options);
  return formatter.format(value);
}
```

[Playground](https://www.typescriptlang.org/play?target=7#code/GYVwdgxgLglg9mABMOAnAtgQygOROgIwFNUAKAN0wBsQiAuRMfY1AGkSszAHMGBnKKhg9EAXkQByImAkBKRAG8AUIkQQEAxHAAOsDWMUrVjOFGzwwDCevTbM0CayOqsADxjp8AMVT29YABEYbhgoPgYARidVAF8AbiN1ME0UDGwoEgMASTAoKgA6PEISLzQsKFJOHnYdfz5ZBNVUIigQVCRU8ozUfM7sCmpaBqUYpSA)

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:

```ts
export default (_, inject: Function) => {
  inject('myPlugin', /* Plugin code */);
};
```

[Playground](https://www.typescriptlang.org/play?target=7#code/KYDwDg9gTgLgBAE2AMwIYFcA28AUB9AGjgEsA7AK2AGMYAuOAMXVJuIlIEo4BeAPjgDeAKDgkK1GDgDkAWwCeABUzoA5mSlEA9ACo4S1WThUISONs0cA3EIC+1oA)

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.

```ts
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.

```ts
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](https://typescript.nuxtjs.org/cookbook/plugins#plugin-2). 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.

```js
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 !](#code-duplication-)). But I also need to describe the `toString` and `currentUser` methods aside.

```ts
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;
};
```

[Playground](https://www.typescriptlang.org/play?pretty=true#code/C4TwDgpgBAogdsAlkiBnKBeKByArqiAJ1WygB8cBDQpAYwBs1sBuAKFEigFUAlAGQBCuRPQAmRTFADerKHKi1chQhARcChABQBKAFzd+QkeMKz5wAPYBlYIURwA5jv2pb9h6wC+UAGTSzcgDaANYQIFD2sAjIiGgAuvqaiKIA-C5ujtqYAHwGgsJiRF6sAPQAVFBWlAC20JSo7ODQvPnGElgy8gpKKmoaznlGhaZdljZ2jgOuEx7efjwQtBaEogA88EgoqAA0UEmp6TNZGLktQybZrGUlrKxLcK5QAIIACgCSkjo5-l2MwFBKeiSbCUMCIFgBBQWB7-ABGBRM+jOCPaPy68kUylUwHURC+nXRhJUwCUcCgwAAFohUAA6fBEVCabAAFgAzAAmACM2G0bEJck820ho2sGScWQJ-PkxNJAMI9D5-M8kM8bEh90e2JiaH0G21qECcUkgTwGhIuxBNEQDCYcUVci1WxpADNljBKLQKZpNI6QMdcpL0fC2oRAr6jVhnbg4LQkNC9sk0lBpu4JcL+YCoABqLAAAxKABIpL7PEXklAUlB82XRJ5c1B9NhsHX7VKoDLCGTKdTW11lX3ebcuh2ycHhmxVbcNRZGDT6BYnLmi683joaZjejj+toadQ6IxGdo64OgA)

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 😎