1413 words
7 minutes
How @tsconfig/strictest + ts-reset changed the way I write TS

There’s a moment in every TypeScript project where you realise something uncomfortable:

“Wait… I thought TypeScript was supposed to catch this.”

That was me.

I thought I was being safe. I had "strict": true in tsconfig.json, I sprinkled types around, and I felt clever.

Then two things happened:

  • Othmane Kinane Theodo Morocco CTO mentioned this package called @total-typescript/ts-reset in a discussion about safer TypeScript.
  • A colleague told me: “If you want to go all-in on type safety, try @tsconfig/strictest.”

So first I tried them on our main project at work.

Because we already had pretty strict quality checks (TS strict mode, lint rules, serious reviews), it didn’t blow up as much as I expected. We found a few things, but nothing dramatic.

I was curious about the real impact, though. So I went to an old personal TypeScript project (a small web app), installed both packages there… and that’s where the big differences showed up.

This article is about those two packages, what they do, and what I actually saw in that old project.


The two packages in plain language#

@tsconfig/strictest#

@tsconfig/strictest is a shared tsconfig preset that turns on basically all the serious safety flags. Think of it as:

"strict": true + “all the extra little things you probably should have enabled but didn’t”.

You don’t have to remember every flag like noImplicitAny, noUncheckedIndexedAccess, exactOptionalPropertyTypes, etc. You just extend this preset and get a “maximum safety” TypeScript config by default.

@total-typescript/ts-reset#

ts-reset is a “standard library reset” for TypeScript.

It doesn’t change your runtime JS at all. It changes how TypeScript types built-ins:

  • JSON.parse
  • Array.prototype.filter(Boolean)
  • includes
  • Object.keys, etc.

It fixes a bunch of places where TS is either too loose (any) or not smart enough, and makes them safer and more predictable.


Setup: how I wired them into my project#

In my old TS project, I did this:

1. Install and extend @tsconfig/strictest#

Terminal window
npm i -D @tsconfig/strictest
# or pnpm add -D @tsconfig/strictest
# or yarn add -D @tsconfig/strictest

Then in tsconfig.json:

{
"extends": "@tsconfig/strictest/tsconfig.json",
"compilerOptions": {
// your custom stuff here
"lib": ["ES2022", "DOM"],
"module": "ESNext",
"moduleResolution": "node"
},
"include": ["src"]
}

Boom — all the strict flags I’d been too lazy to configure are now on.

2. Install and import ts-reset#

Terminal window
npm i -D @total-typescript/ts-reset

Then, in the app entry file (whatever runs first, like src/main.ts or similar):

// Must be the first import
import "@total-typescript/ts-reset";

After these two steps, my editor turned into a “type-safety review” mode. Let’s look at what actually changed.


Where @tsconfig/strictest hit me first#

Example 1 – “I’ll type it later” helper functions#

Old code in my personal project:

export const getUserDisplayName = (user) => {
if (!user) return "Anonymous";
if (user.firstName || user.lastName) {
return `${user.firstName ?? ""} ${user.lastName ?? ""}`.trim();
}
return user.email ?? "Anonymous";
};

This compiled fine before. With @tsconfig/strictest:

  • noImplicitAny complains about user.
  • strict null checks complain about optional fields.
  • The return type is inferred but not guaranteed anywhere.

So I fixed it:

type User = {
firstName?: string | null;
lastName?: string | null;
email?: string | null;
};
export const getUserDisplayName = (user: User | null): string => {
if (!user) return "Anonymous";
if (user.firstName || user.lastName) {
return `${user.firstName ?? ""} ${user.lastName ?? ""}`.trim();
}
return user.email ?? "Anonymous";
};

Now:

  • Callers see clearly that user can be null.
  • The function always returns a string.
  • If I change the User type later, the compiler guides me.

This pattern then went back into our main project: anytime we had “utility functions” with weak types, strictest helped force us to define the contract properly.


Example 2 – Object maps with missing keys#

I had a small config map like this:

const labels = {
home: "Home",
profile: "Profile",
};
function getLabel(route: "home" | "profile" | "settings") {
return labels[route];
}

This kind of “almost correct” code compiled before.

With @tsconfig/strictest, flags like noUncheckedIndexedAccess and stricter indexing made this unsafe:

  • route can be "settings", but labels.settings doesn’t exist.
  • The index access can be undefined.

So the compiler complained, and I refactored:

const labels: Record<RouteName, string> = {
home: "Home",
profile: "Profile",
settings: "Settings", // ✅ compiler forces me to handle all cases
};
type RouteName = "home" | "profile" | "settings";
function getLabel(route: RouteName): string {
return labels[route];
}

Or with satisfies:

const labels = {
home: "Home",
profile: "Profile",
settings: "Settings",
} satisfies Record<RouteName, string>;

Suddenly I can’t “forget” a key without TS warning me — which is exactly what you want in real projects when you add new routes or statuses.


Where ts-reset changed behaviour#

Example 3 – JSON.parse pretending to be safe#

Originally, I had this in my personal project:

const raw = localStorage.getItem("user");
const user = raw ? JSON.parse(raw) : null;
console.log(user.name.toUpperCase());

Default TS types:

  • JSON.parse returns any.
  • user is any or null.
  • TypeScript silently allows user.name.

With ts-reset, JSON.parse returns unknown, so the compiler forces me to be explicit.

I changed it to:

type User = {
id: string;
name: string;
};
const raw = localStorage.getItem("user");
const user: User | null = raw ? (JSON.parse(raw) as User) : null;
if (!user) {
// handle not logged in
} else {
console.log(user.name.toUpperCase());
}

Later, I wrapped it in a helper, but I realised that just using as T wasn’t much better than any. A type assertion is basically telling TypeScript:

“Trust me, it’s definitely a T.”

…without doing any runtime validation:

function parseJson<T>(value: string | null): T | null {
if (!value) return null;
return JSON.parse(value) as T; // ⚠️ No actual validation
}
const user = parseJson<User>(localStorage.getItem("user"));

This is nicer to use, but it’s still unsafe if the JSON in storage doesn’t match User at all.

In reality, the wrapper should also validate what it parses. That can be as simple as a few sanity checks:

function parseUser(value: string | null): User | null {
if (!value) return null;
const parsed: unknown = JSON.parse(value);
// validation logic
}

For real-world projects and more complex types, you don’t want to hand-roll these checks everywhere. That’s where a schema/validation library like Zod shines: you define the shape once, and reuse it for both runtime validation and TypeScript types.

This pattern then migrated back into our main project for:

  • localStorage,
  • feature flags,
  • cached API responses.

ts-reset basically refused to let me pretend that JSON.parse always returns what I think.


Example 4 – .filter(Boolean) and weird arrays#

I had this classic snippet:

const ids = ["1", undefined, "2", null, "3"];
const clean = ids.filter(Boolean);
clean.forEach((id) => {
doSomethingWithId(id);
});

In my head:

clean is an array of strings, obviously.”

In TypeScript’s default world: inference is not always “obviously string[]”.

ts-reset improves the typing of .filter(Boolean) so it matches what we mean:

const ids = ["1", undefined, "2", null, "3"];
const clean = ids.filter(Boolean);
// clean: string[]
clean.forEach((id) => {
// id: string
doSomethingWithId(id);
});

Same code style, but now the types line up with reality. This showed up in a lot of places in my old code where I filtered out nulls/undefined.


Combined effect: API client example#

Here’s a simple API client from my personal project before the changes:

export async function getUser(id: string) {
const res = await fetch(`/api/users/${id}`);
const data = await res.json();
if (!res.ok) {
throw new Error(data.message || "Error");
}
return data.user;
}

Issues:

  • data is any (thanks to res.json() default typing).
  • data.user is “hope-based” access.
  • The caller doesn’t know what getUser returns.

After @tsconfig/strictest + ts-reset, this became:

type User = {
id: string;
name: string;
};
type UserResponse =
| { ok: true; user: User }
| { ok: false; message: string };
export async function getUser(id: string): Promise<User> {
const res = await fetch(`/api/users/${id}`);
// ts-reset makes res.json() safer: we usually assert or validate
const data = (await res.json()) as UserResponse;
if (!data.ok) {
throw new Error(data.message || "Error");
}
return data.user;
}

Now:

  • The function returns Promise<User>.
  • We have a typed “success vs error” shape.
  • If we change User or the API response format, TypeScript catches mismatches.

Again: I first hit this pain in the old project. But the pattern easily transferred into our main project and improved the safety of our API layer there too.


Impact on our main project#

Even though the biggest transformation happened in my old personal project, these two packages still had a good impact on our main project:

  • They confirmed that our strict rules were working: we didn’t see a huge explosion of errors.
  • They helped us find and clean up older, slightly “loose” helpers and types.
  • They improved typings around JSON, array operations, and object maps.
  • They made refactors more confident: when we change a model, the compiler really knows what’s going on.

The nice thing is: you don’t have to rewrite everything. You can introduce these tools, fix the important paths, and then let TypeScript slowly guide you towards better code over time.


If you want to try it yourself#

My honest suggestion:

  1. Pick a small or old TS project first (just like I did).
  2. Add @tsconfig/strictest to tsconfig.json.
  3. Add @total-typescript/ts-reset at the top of your main entry file.
  4. Fix the most central types: shared models, API clients, important helpers.
  5. Once it feels comfortable, bring the same setup (or parts of it) into your main project.