TypeScript Strict Mode skill for Codex
Strict on, without a thousand non-null assertions. Codex measures error counts per strict flag, picks a rollout that fits the codebase (all at once, flag by flag, or a strict tsconfig whose include list only grows), and fixes errors by narrowing, honest types and real defaults rather than any or non-null assertions. It reports suppression counts before and after, and flags any fix that changes runtime behavior.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- typescript-strict-mode Installs to ~/.agents/skills/typescript-strict-mode/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install typescript-strict-mode The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project typescript-strict-mode Installs to .agents/skills/typescript-strict-mode/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/typescript-strict-mode.zipunzip -o typescript-strict-mode.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) typescript-strict-mode PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $typescript-strict-mode, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Turns on TypeScript strict mode in an existing codebase without a big-bang rewrite - flag-by-flag rollout, a strict tsconfig ratchet over migrated files, error budgets in CI, and correct fixes for strictNullChecks, noImplicitAny, strictPropertyInitialization and noUncheckedIndexedAccess instead of any or non-null assertions. Use when a user asks to enable strict, upgrade to TypeScript 6 or 7 where strict became the default, reduce @ts-ignore usage, or fix a wave of "possibly undefined" errors.
Use it when
- A TypeScript project still has strict off or partly off
- An upgrade to TypeScript 6 or 7 surfaced errors because strict became the default
- The codebase leans on any, @ts-ignore or non-null assertions
- You want to add noUncheckedIndexedAccess after strict is clean
Use something else when
- You are stuck on one hard compiler error rather than a rollout: use typescript-type-errors
- The work is lint rules such as no-explicit-any configuration: use eslint-setup
How to check the result
tsc --noEmit passes in CI including the strict ratchet config, and counts of any, @ts-expect-error and non-null assertions did not rise.
The mistake it prevents
Silencing strictNullChecks errors with the non-null assertion operator, which turns compile-time warnings back into runtime crashes.
Done well means: strict: true is on in the main tsconfig, CI fails on new errors, each
fix encodes a real invariant (a narrowing, a default, a guard), and the number of any,
! and @ts-expect-error did not go up to get there.
Before changing anything
- Read
AGENTS.mdfor the typecheck command (tsc --noEmit,pnpm typecheck,vue-tsc, project references withtsc -b). - Inspect every tsconfig (
tsconfig*.json,extendschains, per-package configs in a monorepo) and the TypeScript version in the lockfile. - TypeScript 6.0 made
strictthe default when it is not set, and 7.0 (the Go-based native compiler) keeps that default. A repo upgrading with nostrictkey suddenly gets strict errors; set"strict": falseexplicitly as a stopgap and follow the rollout below. - Measure per flag, so you know where the cost is:
for f in noImplicitAny strictNullChecks strictFunctionTypes strictBindCallApply \
strictPropertyInitialization noImplicitThis useUnknownInCatchVariables; do
n=$(npx tsc --noEmit --pretty false --$f 2>/dev/null | grep -c 'error TS')
echo "$f $n"
done
What strict turns on
| Flag | Typical error volume | Fix pattern |
|---|---|---|
noImplicitAny | High in JS-ported code | Annotate parameters; type JSON with interfaces or a schema |
strictNullChecks | Highest, most valuable | Narrow, default, or change the type to be honest |
strictFunctionTypes | Low | Callback parameter types were too wide; fix the declared type |
strictBindCallApply | Low | Real argument mismatches |
strictPropertyInitialization | Medium in class-heavy code | Initialize in constructor, make optional, or declare for framework-injected fields |
noImplicitThis | Low | Arrow functions or a this: parameter |
useUnknownInCatchVariables | Medium | Narrow err with instanceof Error |
alwaysStrict | None | Emits "use strict" |
strictBuiltinIteratorReturn (5.6+) | Low | Iterator results typed with undefined return |
Recommended extras after strict is clean, in this order: noImplicitOverride,
noFallthroughCasesInSwitch, noUncheckedIndexedAccess (high volume, high value),
exactOptionalPropertyTypes (only for libraries that care about undefined vs missing).
Rollout strategy
| Codebase | Strategy |
|---|---|
| < 200 errors total | Turn strict: true on and fix everything in one PR series |
| Errors concentrated in one or two flags | Enable flags one at a time in the main config, cheapest first; strictNullChecks last |
| Thousands of errors spread everywhere | Strict ratchet config over migrated files (below), or file-level opt-out with typescript-strict-plugin (// @ts-strict-ignore headers that only shrink) |
Strict ratchet with two configs:
// tsconfig.strict.json — CI runs this too
{
"extends": "./tsconfig.json",
"compilerOptions": { "strict": true, "noEmit": true },
"include": [
"src/lib/**/*",
"src/features/billing/**/*"
]
}
{ "scripts": { "typecheck": "tsc --noEmit && tsc -p tsconfig.strict.json" } }
Caveat: files in include pull in their imports, so errors in non-migrated dependencies
appear too. Migrate leaves first (utils, types, API clients), then work upward. Each PR adds
paths to include; none removes them. When include covers src, flip the main config and
delete the extra one.
Error-budget alternative: commit the current error count and fail CI if it rises.
count=$(npx tsc --noEmit --pretty false -p tsconfig.strict-all.json | grep -c 'error TS')
test "$count" -le "$(cat .strict-budget)" || { echo "strict errors rose to $count"; exit 1; }
Fixing errors properly
Possibly undefined / null (TS18048, TS2532, TS18047)
// Bad: hides the bug
const user = users.find(u => u.id === id)!;
// Good: decide what absence means
const user = users.find(u => u.id === id);
if (!user) throw new NotFoundError(`user ${id}`);
// Good: a real default
const pageSize = options.pageSize ?? 50;
// Good: the type was lying; make it honest at the source
interface Order { shippedAt: Date | null } // and handle null where it is read
Use ! only where an invariant the compiler cannot see is guaranteed nearby (for example
right after a Map.has check on the same key), and prefer restructuring even then:
const v = map.get(k); if (v !== undefined) ....
Implicit any (TS7006, TS7031)
Annotate function parameters; let return types and locals infer. For external JSON, do not
write any: declare an interface and validate at the boundary (Zod, Valibot, or a type guard).
Class properties (TS2564)
class Service {
private client: HttpClient; // initialize in constructor
private cache?: Map<string, Item>; // genuinely optional
declare readonly config: Config; // set by a framework/decorator, no emit
constructor(client: HttpClient) { this.client = client; }
}
Definite assignment name!: string is acceptable only for fields assigned by frameworks
(Angular @Input, ORM entities) and is better replaced with the framework’s own pattern.
Catch variables (TS18046)
try { await save(); }
catch (err) {
const message = err instanceof Error ? err.message : String(err);
logger.error({ err }, message);
throw err;
}
noUncheckedIndexedAccess
arr[i] and record[key] become T | undefined. Prefer for...of, .at() with a check,
destructuring with defaults, or Map for dynamic keys. Do not blanket-add !.
Suppressions policy
@ts-expect-errorwith a reason, never@ts-ignore: it fails when the error disappears.- Enforce with typescript-eslint:
@typescript-eslint/ban-ts-comment(require descriptions),no-explicit-any,no-non-null-assertionas warn during migration, error after. - Count suppressions before and after; report both numbers.
rg -c '@ts-expect-error|@ts-ignore|: any\b|as any\b|!\.' src | awk -F: '{s+=$2} END {print s}'
Gotchas
skipLibCheck: truehides errors in.d.tsfiles, including your own generated ones. Keep it on for speed but run one CI job without it if you publish types.- Turning on
strictNullCheckschanges inferred types (findreturnsT | undefined), which can surface errors in files nobody touched. Expect fan-out. - Frameworks with their own checker (
vue-tsc,svelte-check, AngularstrictTemplates) need the same flags enabled there, or templates stay unchecked. On TypeScript 7 these tools may still run a 6.x compiler side by side; keep both on the same tsconfig. - Project references (
tsc -b): every referenced project needs the flag; the root config does not propagate compiler options to them. - Do not change runtime behavior while fixing types. A new
throwor default is a behavior change; call it out in the PR and add a test.
Verification
tsc --noEmit(and the strict ratchet config) exits 0 in CI.- Suppression and
anycounts are reported before and after; they did not rise. - Tests pass; any fix that added a guard or default has a test for the absent case.
PR summary template
**Scope:** strictNullChecks for src/features/billing (+38 files in tsconfig.strict.json)
**Errors fixed:** 214 (narrowing 120, honest types 61, defaults 25, guards with tests 8)
**Suppressions:** @ts-expect-error 3 -> 3, non-null assertions 41 -> 12, any 17 -> 9
**Behavior changes:** NotFoundError now thrown in getInvoice when customer missing (test added) What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "TypeScript Strict Mode"
short_description: "Strict on, without a thousand non-null assertions"
default_prompt: "$typescript-strict-mode Turn on strict mode in this TypeScript repo without one giant PR" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.