Ban Type Assertions
Enable typescript/consistent-type-assertions with assertionStyle: 'never'
(native oxlint, v1.44.0+) and replace all as X / <T>x casts with patterns
the compiler can verify. as const is preserved — the ban is surgical: it
targets real casts, not literal narrowing.
Core Philosophy
Pick the strictly correct path, not the simpler one.
Every as assertion is a spot where the developer told the compiler "trust me."
The goal is to make the compiler verify instead. If you replace as Foo with a
type guard that is equally unverified, you have not improved anything — you have
just moved the assertion.
Quick Reference
- Rule:
typescript/consistent-type-assertions - Config:
{ assertionStyle: 'never' } - Enforcement: native oxlint (v1.44.0+) — no jsPlugins, no eslint sliver
(per the
typescript.mdstandard, decision #6). Part of the shared.oxlintrc.json.
Workflow
1. Enable the Rule
Add to the project's .oxlintrc.json (oxlint flat config):
{
"rules": {
"typescript/consistent-type-assertions": ["error", { "assertionStyle": "never" }]
}
}
Run type-aware: npx oxlint --type-aware . (the oxlint-tsgolint package drives
the real TypeScript compiler; needs TypeScript 7.0+).
2. Enumerate Violations
npx oxlint --type-aware . 2>&1 | grep "consistent-type-assertions"
Group violations by file and pattern before fixing.
3. Research Before Fixing
Before writing any replacement code:
- Check for existing zod schemas — grep for
Schemaalongside the type name across the project. - Check if schemas exist but aren't exported — if so, export them rather than creating new ones.
- Check for duplicate types/interfaces — consolidate into a shared schemas module if found.
- Understand the data flow — is this a parse boundary (external data), a narrowing site (union type), or a library type gap?
4. Fix Violations Using the Pattern Hierarchy
Tier 1: Zod Parsing (for external data boundaries)
Use for any data entering the system from JSON, disk, network, IPC, etc. This gives runtime validation, not just a type annotation.
// BAD
const data = JSON.parse(raw) as MyType;
// GOOD
const data = MySchema.parse(JSON.parse(raw));
Use safeParse when you need to handle errors gracefully (e.g., returning an
error response with context like a request id):
// BAD: throws before you can extract the request id
const request = RequestSchema.parse(JSON.parse(raw));
// GOOD: safeParse lets you return a proper error
const parsed = RequestSchema.safeParse(JSON.parse(raw));
if (!parsed.success) {
return errorResponse(rawObj?.id ?? null, INVALID_PARAMS, parsed.error.message);
}
const request = parsed.data;
Tier 2: Control Flow Narrowing (for union types)
Use switch, in, instanceof, or discriminated unions:
// BAD
(error as NodeJS.ErrnoException).code
// GOOD
if (error instanceof Error && 'code' in error) {
const code = error.code;
}
// BAD
if (METHODS.has(method as Method)) { ... }
// GOOD: switch narrows exhaustively
switch (method) {
case 'foo':
case 'bar':
return handle(method); // narrowed
}
Tier 3: oxlint-disable with Justification (last resort)
Only for genuinely unavoidable cases (library type gaps, generic parameters that can't be inferred). Always explain why:
// oxlint-disable-next-line typescript/consistent-type-assertions -- ws library types require a generic parameter
ws.on('message', handler);
Anti-Pattern: Type Guards That Are Disguised Assertions
// NOT an improvement -- checks shape but not content
function isDaemonRequest(x: unknown): x is DaemonRequest {
return typeof x === 'object' && x !== null && 'method' in x;
}
A zod schema validates values. A type guard like this is an unverified assertion with extra steps. Only use type guards when the narrowing logic is truly sufficient.
5. Use Strict Schemas, Not Permissive Ones
When a schema exists (e.g., SessionSettingsSchema), use it strictly rather than
z.record(z.unknown()). This ensures forward compatibility — if fields are
removed in a migration, stale data gets cleaned on read.
// BAD: accepts anything
const settings = z.record(z.unknown()).parse(raw);
// GOOD: validates against the real shape
const settings = SessionSettingsSchema.parse(raw);
6. Fix Test Mocks to Match Schemas
Once you replace as X with .parse(), test mocks that relied on the assertion
will fail validation. Fix the mocks — do not disable the rule in tests.
Create helper functions to centralize valid test fixtures:
function mockSessionSummary(
overrides?: Partial<SessionSummaryEvent>,
): SessionSummaryEvent {
return {
type: 'session_start',
id: 'test-id',
title: 'Test Session',
owner: 'test-owner',
...overrides,
};
}
7. Parse at the Boundary, Inside Error Handling
Make sure parsing happens where failures produce proper error responses, not unhandled exceptions:
// BAD: parse outside try/catch -- if it throws, you lose context
const request = RequestSchema.parse(data);
try { handle(request); } catch { ... }
// GOOD: safeParse before try, handle error with context
const parsed = RequestSchema.safeParse(data);
if (!parsed.success) {
return errorResponse(rawData?.id ?? null, INVALID_PARAMS, parsed.error.message);
}
try { handle(parsed.data); } catch { ... }
Verification
npx oxlint --type-aware . # lint — the native rule fires
npx tsc --noEmit # typecheck
npm test # tests — mocks must match schemas
# knip is opt-in (typescript.md [TS-43]) — run it for larger projects / monorepos
Reminders
- Exported enums live in
enums.ts(@factory/enum-file-organization/structure.md[ST-01]); for string-union data at a boundary, use a schema + enum, not anascast. - Prefer fixing mocks over disabling the rule; a scoped
oxlint-disableis the last-resort escape hatch — never a blanket disable. - This rule bans
as X/<T>x;as constis unaffected. Non-null!assertions are a separate, softer review policy (seetypescript.md[TS-17]).
Adaptation note (2026-07, decision #11). Derived from Factory-AI's
ban-type-assertionsskill. Step 1 was rewritten from the original@typescript-eslint/consistent-type-assertionsinpackages/<name>/.eslintrc.jsto native oxlint (decision #6: no eslint sliver). The original §6 "Promote Shared Schemas to@factory/common" (Factory monorepo: subpathpackage.jsonexports +no-barrel-files+ repo-rootnpm run knip) was pruned — it does not apply to this consumer stack. The portable core (philosophy, pattern hierarchy, strict schemas, test-mock fixing, parse-at-boundary) is retained.