ts-reset

A 'CSS reset' for TypeScript, improving types for common JavaScript API's

README

ts-reset


TypeScript's built-in typings are not perfect. ts-resetmakes them better.

Without ts-reset:

🚨* .json(in fetch) and JSON.parseboth return any
🤦* .filter(Boolean)doesn't behave how you expect
😡* array.includesoften breaks on readonly arrays

ts-resetsmooths over these hard edges, just like a CSS reset does in the browser.

With ts-reset:

👍* .json(in fetch) and JSON.parseboth return unknown
✅* .filter(Boolean)behaves EXACTLY how you expect
🥹 array.includesis widened to be more ergonomic
🚀* And several more changes!

Example


  1. ``` ts
  2. // Import in a single file, then across your whole project...
  3. import "@total-typescript/ts-reset";

  4. // .filter just got smarter!
  5. const filteredArray = [1, 2, undefined].filter(Boolean); // number[]

  6. // Get rid of the any's in JSON.parse and fetch
  7. const result = JSON.parse("{}"); // unknown

  8. fetch("/")
  9.   .then((res) => res.json())
  10.   .then((json) => {
  11.     console.log(json); // unknown
  12.   });
  13. ```

Get Started


Install: npm i -D @total-typescript/ts-reset

Create a reset.d.tsfile in your project with these contents:

  1. ``` ts
  2. // Do not add any other lines of code to this file!
  3. import "@total-typescript/ts-reset";
  4. ```

Enjoy improved typings across your entireproject.

Installing only certain rules


By importing from @total-typescript/ts-reset, you're bundling allthe recommended rules.

To only import the rules you want, you can import like so:

  1. ``` ts
  2. // Makes JSON.parse return unknown
  3. import "@total-typescript/ts-reset/json-parse";

  4. // Makes await fetch().then(res => res.json()) return unknown
  5. import "@total-typescript/ts-reset/fetch";
  6. ```

For these imports to work, you'll need to ensure that, in your tsconfig.json, moduleis set to NodeNextor Node16.

Below is a full list of all the rules available.

Caveats


Use ts-resetin applications, not libraries


ts-resetis designed to be used in application code, not library code. Each rule you include will make changes to the global scope. That means that, simply by importing your library, your user will be unknowingly opting in to ts-reset.

Rules


Make JSON.parsereturn unknown


  1. ``` ts
  2. import "@total-typescript/ts-reset/json-parse";
  3. ```

JSON.parsereturning anycan cause nasty, subtle bugs. Frankly, any any's can cause bugs because they disable typechecking on the values they describe.

  1. ``` ts
  2. // BEFORE
  3. const result = JSON.parse("{}"); // any
  4. ```

By changing the result of JSON.parseto unknown, we're now forced to either validate the unknownto ensure it's the correct type (perhaps using zod ), or cast it with as.

  1. ``` ts
  2. // AFTER
  3. import "@total-typescript/ts-reset/json-parse";

  4. const result = JSON.parse("{}"); // unknown
  5. ```

Make .json()return unknown


  1. ``` ts
  2. import "@total-typescript/ts-reset/fetch";
  3. ```

Just like JSON.parse, .json()returning anyintroduces unwanted any's into your application code.

  1. ``` ts
  2. // BEFORE
  3. fetch("/")
  4.   .then((res) => res.json())
  5.   .then((json) => {
  6.     console.log(json); // any
  7.   });
  8. ```

By forcing res.jsonto return unknown, we're encouraged to distrust its results, making us more likely to validate the results of fetch.

  1. ``` ts
  2. // AFTER
  3. import "@total-typescript/ts-reset/fetch";

  4. fetch("/")
  5.   .then((res) => res.json())
  6.   .then((json) => {
  7.     console.log(json); // unknown
  8.   });
  9. ```

Make .filter(Boolean)filter out falsy values


  1. ``` ts
  2. import "@total-typescript/ts-reset/filter-boolean";
  3. ```

The default behaviour of .filtercan feel pretty frustrating. Given the code below:

  1. ``` ts
  2. // BEFORE
  3. const filteredArray = [1, 2, undefined].filter(Boolean); // (number | undefined)[]
  4. ```

It feels natural that TypeScript should understand that you've filtered out the undefinedfrom filteredArray. You can make this work, but you need to mark it as a type predicate:

  1. ``` ts
  2. const filteredArray = [1, 2, undefined].filter((item): item is number => {
  3.   return !!item;
  4. }); // number[]
  5. ```

Using .filter(Boolean)is a really common shorthand for this. So, this rule makes it so .filter(Boolean)acts like a type predicate on the array passed in, removing any falsy values from the array member.

  1. ``` ts
  2. // AFTER
  3. import "@total-typescript/ts-reset/filter-boolean";

  4. const filteredArray = [1, 2, undefined].filter(Boolean); // number[]
  5. ```

Make .includeson as constarrays less strict


  1. ``` ts
  2. import "@total-typescript/ts-reset/array-includes";
  3. ```

This rule improves on TypeScript's default .includesbehaviour. Without this rule enabled, the argument passed to .includesMUST be a member of the array it's being tested against.

  1. ``` ts
  2. // BEFORE
  3. const users = ["matt", "sofia", "waqas"] as const;

  4. // Argument of type '"bryan"' is not assignable to
  5. // parameter of type '"matt" | "sofia" | "waqas"'.
  6. users.includes("bryan");
  7. ```

This can often feel extremely awkward. But with the rule enabled, .includesnow takes a widened version of the literals in the constarray.

  1. ``` ts
  2. // AFTER
  3. import "@total-typescript/ts-reset/array-includes";

  4. const users = ["matt", "sofia", "waqas"] as const;

  5. // .includes now takes a string as the first parameter
  6. users.includes("bryan");
  7. ```

This means you can test non-members of the array safely.

Make .indexOfon as constarrays less strict


  1. ``` ts
  2. import "@total-typescript/ts-reset/array-index-of";
  3. ```

Exactly the same behaviour of .includes(explained above), but for .lastIndexOfand .indexOf.

Make Set.has()less strict


  1. ``` ts
  2. import "@total-typescript/ts-reset/set-has";
  3. ```

Similar to .includes, Set.has()doesn't let you pass members that don't exist in the set:

  1. ``` ts
  2. // BEFORE
  3. const userSet = new Set(["matt", "sofia", "waqas"] as const);

  4. // Argument of type '"bryan"' is not assignable to
  5. // parameter of type '"matt" | "sofia" | "waqas"'.
  6. userSet.has("bryan");
  7. ```

With the rule enabled, Setis much smarter:

  1. ``` ts
  2. // AFTER
  3. import "@total-typescript/ts-reset/set-has";

  4. const userSet = new Set(["matt", "sofia", "waqas"] as const);

  5. // .has now takes a string as the argument!
  6. userSet.has("bryan");
  7. ```

Make Map.has()less strict


  1. ``` ts
  2. import "@total-typescript/ts-reset/map-has";
  3. ```

Similar to .includesor Set.has(), Map.has()doesn't let you pass members that don't exist in the map's keys:

  1. ``` ts
  2. // BEFORE
  3. const userMap = new Map([
  4.   ["matt", 0],
  5.   ["sofia", 1],
  6.   [2, "waqas"],
  7. ] as const);

  8. // Argument of type '"bryan"' is not assignable to
  9. // parameter of type '"matt" | "sofia" | "waqas"'.
  10. userMap.has("bryan");
  11. ```

With the rule enabled, Mapfollows the same semantics as Set.

  1. ``` ts
  2. // AFTER
  3. import "@total-typescript/ts-reset/map-has";

  4. const userMap = new Map([
  5.   ["matt", 0],
  6.   ["sofia", 1],
  7.   [2, "waqas"],
  8. ] as const);

  9. // .has now takes a string as the argument!
  10. userMap.has("bryan");
  11. ```

Removing any[]from Array.isArray()


  1. ``` ts
  2. import "@total-typescript/ts-reset/is-array";
  3. ```

When you're using Array.isArray, you can introduce subtle any's into your app's code.

  1. ``` ts
  2. // BEFORE

  3. const validate = (input: unknown) => {
  4.   if (Array.isArray(input)) {
  5.     console.log(input); // any[]
  6.   }
  7. };
  8. ```

With is-arrayenabled, this check will now mark the value as unknown[]:

  1. ``` ts
  2. // AFTER
  3. import "@total-typescript/ts-reset/is-array";

  4. const validate = (input: unknown) => {
  5.   if (Array.isArray(input)) {
  6.     console.log(input); // unknown[]
  7.   }
  8. };
  9. ```

Rules we won't add


Object.keys/Object.entries


A common ask is to provide 'better' typings for `Object.keys`, so that it returns `Array`instead of `Array`. Same for `Object.entries`. `ts-reset`won't be including rules to change this.

TypeScript is a structural typing system. One of the effects of this is that TypeScript can't always guarantee that your object types don't contain excess properties:

  1. ``` ts
  2. type Func = () => {
  3.   id: string;
  4. };

  5. const func: Func = () => {
  6.   return {
  7.     id: "123",
  8.     // No error on an excess property!
  9.     name: "Hello!",
  10.   };
  11. };
  12. ```

So, the only reasonable type for `Object.keys`to return is `Array`.

Generics for JSON.parse, Response.jsonetc


A common request is for ts-resetto add type arguments to functions like JSON.parse:

  1. ``` ts
  2. const str = JSON.parse<string>('"hello"');

  3. console.log(str); // string
  4. ```

This appears to improve the DX by giving you autocomplete on the thing that gets returned from JSON.parse.

However, we argue that this is a lie to the compiler and so, unsafe.

JSON.parseand fetchrepresent validation boundaries- places where unknown data can enter your application code.

If you reallyknow what data is coming back from a JSON.parse, then an asassertion feels like the right call:

  1. ``` ts
  2. const str = JSON.parse('"hello"') as string;

  3. console.log(str); // string
  4. ```

This provides the types you intend and also signals to the developer that this is slightlyunsafe.