| name | strict-typescript |
| description | Configures TypeScript compiler and type-level patterns beyond strict:true to make unsafe code fail at compile time. Use when setting up or hardening tsconfig.json, choosing compiler flags (noUncheckedIndexedAccess, exactOptionalPropertyTypes, verbatimModuleSyntax, erasableSyntaxOnly), eliminating any/as, installing ts-reset or type-fest, writing branded/template-literal/conditional types, or wiring @typescript-eslint strict-type-checked. |
| version | 1.2.0 |
| libraries | ["@total-typescript/ts-reset","type-fest"] |
Strict TypeScript
Overview
strict: true is the floor, not the ceiling. It still lets through unchecked index access, undefined-as-present optional properties, runtime-only syntax, and any leaks from the standard library. Patterns without compile-time enforcement are suggestions: an agent or a tired engineer will drift from them the moment types stop complaining.
This skill turns intent into compiler errors: a hardened tsconfig.json, ts-reset to plug any leaks, type-level patterns (satisfies, branded types, discriminated unions) that encode invariants, and @typescript-eslint rules that ban the escape hatches. The stricter the config, the less the type system has to guess, which also makes the compiler faster.
When to Use
- Bootstrapping or auditing a
tsconfig.json for a production project
- Deciding which compiler flags to enable beyond
strict: true
- Eliminating
any / as from a codebase
- Encoding domain invariants in types (IDs that can't be swapped, exhaustive unions)
- Enforcing type safety at build time via ESLint so violations fail CI
When NOT to use: prototypes or throwaway scripts where iteration speed beats safety, or runtime input validation, which belongs at the boundary with Zod (see validation-boundary).
Related: pattern-enforcement (ESLint architectural rules), validation-boundary (runtime parsing of unknown), fn-args-deps (the pattern verbatimModuleSyntax protects), result-types (discriminated unions for errors).
Required tsconfig.json
{
"compilerOptions": {
"target": "ES2024",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"verbatimModuleSyntax": true,
"erasableSyntaxOnly": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
Key Flags Explained
noUncheckedIndexedAccess
By default, myArray[0] is typed as the element type. This is a lie - it could be undefined.
const users = ['Alice', 'Bob'];
const first = users[0];
const first = users[0];
if (first) {
console.log(first.toUpperCase());
}
exactOptionalPropertyTypes
Ensures { id?: string } means the key is MISSING, not undefined.
type User = { id?: string };
const user: User = { id: undefined };
const user: User = { id: undefined };
const user: User = {};
erasableSyntaxOnly (TS 5.8+)
Ensures code is compatible with native TypeScript runners (Node.js 22+, Bun, Deno):
enum Status { Active, Inactive }
class User { constructor(public name: string) {} }
const Status = { Active: 'active', Inactive: 'inactive' } as const;
type Status = (typeof Status)[keyof typeof Status];
class User {
name: string;
constructor(name: string) {
this.name = name;
}
}
verbatimModuleSyntax
Enforces import type for types - critical for the fn(args, deps) pattern:
import type { Database } from '../infra/database';
type GetUserDeps = { db: Database };
async function getUser(args, deps: GetUserDeps) {
return deps.db.findUser(args.userId);
}
import { db } from '../infra/database';
async function getUser(args) {
return db.findUser(args.userId);
}
noUncheckedSideEffectImports
Catches ghost imports - side-effect imports that reference deleted files:
import "./polyfills";
import "reflect-metadata";
Why this matters:
- Side-effect imports run code but don't export anything
- Without this flag, TypeScript ignores them entirely
- Deleted or renamed files cause silent runtime failures
- This flag ensures all imports resolve correctly
Fix Standard Library Leaks with ts-reset
JSON.parse returns any by default - bypasses all your validation!
npm install -D @total-typescript/ts-reset
Create reset.d.ts:
import "@total-typescript/ts-reset";
Now:
const data = JSON.parse(input);
const data = JSON.parse(input);
const user = UserSchema.parse(data);
Also fixes:
[1, undefined, 2].filter(Boolean);
[1, undefined, 2].filter(Boolean);
Type-Level Patterns
satisfies Operator
const routes = {
home: { path: '/', handler: () => {} },
about: { path: '/about', handler: () => {} },
} satisfies Record<string, Route>;
routes.typo;
routes.home;
as const Assertions
const ROLES = ['admin', 'user', 'guest'] as const;
type Role = (typeof ROLES)[number];
type-fest Utility Types
npm install type-fest
import type { Simplify, SetRequired, PartialDeep, ReadonlyDeep } from 'type-fest';
type UserWithPosts = Simplify<User & { posts: Post[] }>;
type CreateUserArgs = SetRequired<Partial<User>, 'email' | 'name'>;
type UserPatch = PartialDeep<User>;
type ImmutableUser = ReadonlyDeep<User>;
Developer Experience
Complex type errors are a primary cause of pattern abandonment. Two tools help:
Total TypeScript VS Code Extension: Translates obtuse TypeScript errors into plain language directly in the IDE. Essential when working with complex generics like createWorkflow error unions.
Type queries: Use // ^? comments to show types inline in your editor:
const user = { id: '123', role: 'admin' } as const;
This helps engineers understand complex generics and ensures code samples are truthful.
The Native Compiler Future
As of late 2025, the TypeScript team is porting the compiler to native code (the "tsgo" project) to achieve up to 10x speedups. This native compiler uses multi-threading and optimized memory layouts.
Why stricter flags matter for performance: Flags like verbatimModuleSyntax and erasableSyntaxOnly reduce the "heuristics" the compiler needs to perform. When the compiler doesn't have to guess whether an import is type-only, or whether a feature needs transpilation, it can take faster code paths.
import type { User } from './types';
import { db } from './database';
The flags we recommend aren't only about safety. They also improve performance: stricter code is faster to compile because it's more explicit about intent.
ESLint Enforcement
Ban unsafe patterns with tooling:
{
extends: ['plugin:@typescript-eslint/strict-type-checked'],
rules: {
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unsafe-argument': 'error',
'@typescript-eslint/no-unsafe-assignment': 'error',
'@typescript-eslint/no-unsafe-call': 'error',
'@typescript-eslint/no-unsafe-member-access': 'error',
'@typescript-eslint/no-unsafe-return': 'error',
'@typescript-eslint/consistent-type-assertions': ['error', { assertionStyle: 'never' }],
'@typescript-eslint/no-non-null-assertion': 'error'
}
}
Type Narrowing (Never Use as)
WRONG: Type Assertion
const user = data as User;
CORRECT: Type Guard
function isUser(x: unknown): x is User {
return typeof x === 'object' && x !== null && 'id' in x;
}
if (isUser(data)) {
data.id;
}
CORRECT: Discriminated Union
type ApiResponse =
| { status: 'success'; data: User }
| { status: 'error'; message: string };
function handleResponse(response: ApiResponse) {
if (response.status === 'success') {
response.data;
}
}
CORRECT: Zod Validation
const data: unknown = JSON.parse(input);
const user = UserSchema.parse(data);
Advanced Type Patterns
Branded Types
Compile-time distinction between primitives:
type UserId = string & { __brand: 'UserId' };
type PostId = string & { __brand: 'PostId' };
function getUser(id: UserId): User { }
function getPost(id: PostId): Post { }
const userId = 'abc' as UserId;
const postId = 'xyz' as PostId;
getUser(userId);
getUser(postId);
Template Literal Types
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Route = `/${string}`;
type Endpoint = `${HttpMethod} ${Route}`;
const endpoint: Endpoint = 'GET /users';
const invalid: Endpoint = 'FETCH /users';
Conditional Types
type ApiResponse<T> = T extends Error
? { success: false; error: T }
: { success: true; data: T };
Mapped Types with Key Remapping
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<{ name: string; age: number }>;
Build Performance
Avoid Barrel Files
export * from './user';
export * from './post';
export * from './comment';
import { User } from './user';
import { Post } from './post';
Profile with Diagnostics
tsc --extendedDiagnostics
Project References for Monorepos
{
"references": [
{ "path": "./packages/shared" },
{ "path": "./packages/api" }
]
}
Build Tools
| Tool | Use For |
|---|
| Vite | Modern dev server, HMR (apps) |
| tsup/esbuild | Ultra-fast transpilation (libraries) |
| tsc | Type checking (always, regardless of bundler) |
Common Rationalizations
| Rationalization | Reality |
|---|
"strict: true is enough" | It leaves index access, optional properties, and JSON.parse unsafe. The gaps are where production bugs live. |
"I'll just cast it with as for now" | as tells the compiler to stop checking. The mismatch it hides surfaces at runtime instead. Use a type guard or Zod. |
"noUncheckedIndexedAccess adds too many undefined checks" | Those checks are the bugs you didn't write yet. arr[i] can be undefined. |
| "Enums are fine, everyone uses them" | They emit JavaScript and break native TS runners. as const objects are erasable and give the same safety. |
| "Branded types are overkill" | A UserId passed where a PostId is expected is a silent data-corruption bug a brand catches for free. |
| "Barrel files keep imports tidy" | They wreck tree-shaking and breed circular dependencies. Import directly. |
Red Flags
tsconfig.json with strict: true but none of the supplementary flags
as assertions or any outside of test fixtures
// @ts-ignore / // @ts-expect-error without an explanation comment
enum or constructor parameter properties (constructor(public x)): not erasable
JSON.parse(...) whose result flows into logic without validation
index.ts files that only re-export (export * from './x')
- ESLint type-safety rules set to
'warn' instead of 'error'
Verification