preen-typescript
Preen TypeScript Types
Proactively search the monorepo for weak TypeScript typings and strengthen them by replacing any, narrowing unknown, removing unsafe as casts, and eliminating @ts-ignore/@ts-expect-error comments.
When to Run
Run this skill when maintaining code quality or during slack time. It searches the entire codebase for type safety improvements.
Discovery Phase
Search all packages for files with type safety issues:
# Find files with `any` type annotations
rg -n --glob '*.{ts,tsx}' ': any|: any\[\]|<any>|as any' . | wc -l
# List specific files with `any`
rg -l --glob '*.{ts,tsx}' ': any|: any\[\]|<any>|as any' . | head -20
# Find `as` type assertions (potential unsafe casts)
rg -n --glob '*.{ts,tsx}' ' as [A-Z]' . | rg -v '\.test\.' | head -20
# Find ts-ignore and ts-expect-error comments
rg -n --glob '*.{ts,tsx}' '@ts-ignore|@ts-expect-error' . | head -20
# Find `unknown` that might need narrowing
rg -n --glob '*.{ts,tsx}' ': unknown' . | head -20
Prioritization
Fix issues in this order (highest impact first):
anyin function signatures - Parameters and return types affect all callersanyin exported interfaces/types - Affects consuming code across the codebaseascasts that bypass type checking - Often hide real bugs@ts-ignore/@ts-expect-error- Usually indicate underlying issuesanyin local variables - Lower impact but still worth fixingunknownthat needs narrowing - Usually safe but could be more precise
Replacement Strategies
Replacing any
- Infer from usage: Look at how the value is used to determine the correct type
- Use generics: Replace
anywith type parameters when the type varies - Use union types: When a value can be one of several known types
- Use
unknown: When the type is truly unknown at compile time (then narrow it)
// Before
function process(data: any): any { ... }
// After - with proper types
function process(data: UserInput): ProcessedResult { ... }
// After - with generics
function process<T extends BaseData>(data: T): ProcessedData<T> { ... }
Replacing as Casts
- Use type guards: Write functions that narrow types safely
// Before
const user = response.data as User;
// After - with type guard
function isUser(value: unknown): value is User {
if (typeof value !== 'object' || value === null) {
return false;
}
const obj = value as Record<string, unknown>;
return (
typeof obj.id === 'string' &&
typeof obj.email === 'string'
);
}
if (isUser(response.data)) {
const user = response.data; // Type is narrowed to User
}
- Use assertion functions: For cases where you want to throw on invalid data
function assertIsUser(value: unknown): asserts value is User {
if (!isUser(value)) {
throw new Error('Expected User object');
}
}
- Fix the source: Often the upstream type is wrong and should be fixed there
Eliminating @ts-ignore / @ts-expect-error
- Fix the actual type error: Usually the right solution
- Update type definitions: If the types are wrong
- Add proper overloads: If the function signature is incomplete
- Use type assertions as last resort: With a comment explaining why
// Before
// @ts-ignore - TODO fix this
const result = someFunction(data);
// After - fix the root cause
const result = someFunction(data as ExpectedType); // Temporary: upstream types are incorrect, see issue #123
Narrowing unknown
Use type guards, typeof, instanceof, or discriminated unions:
// Before
function handle(error: unknown) {
console.log(error.message); // Error: 'unknown' has no property 'message'
}
// After
function handle(error: unknown) {
if (error instanceof Error) {
console.log(error.message);
} else if (typeof error === 'string') {
console.log(error);
} else {
console.log('Unknown error:', String(error));
}
}
Validating Network/Storage Data
Always validate data from external sources (SSE, WebSocket, Redis, localStorage):
// Before - trusting external data
const parsed = JSON.parse(data) as StoredUser;
// After - validate before use
interface StoredUser {
id: string;
email: string;
}
function isStoredUser(value: unknown): value is StoredUser {
return (
isRecord(value) &&
typeof value['id'] === 'string' &&
typeof value['email'] === 'string'
);
}
const parsed: unknown = JSON.parse(data);
if (!isStoredUser(parsed)) {
return null; // or throw
}
// parsed is now safely typed as StoredUser
Cleaner Global Type Access
Use globalThis with a typed intersection instead of window as unknown as {...}:
// Before - verbose and uses `unknown`
const handlers = (
window as unknown as {
__messageHandler?: Map<string, (msg: Message) => void>;
}
).__messageHandler;
// After - cleaner with globalThis
type MessageHandlerRegistry = Map<string, (msg: Message) => void>;
const handlers = (
globalThis as { __messageHandler?: MessageHandlerRegistry }
).__messageHandler;
Type Guard Patterns
Place type guards in a shared location when used across files:
packages/shared/src/typeGuards/
user.ts # isUser, assertIsUser
api.ts # isApiResponse, isApiError
index.ts # Re-exports
Standard Type Guard Template
export function isTypeName(value: unknown): value is TypeName {
if (typeof value !== 'object' || value === null) {
return false;
}
const obj = value as Record<string, unknown>;
return (
typeof obj.requiredField === 'string' &&
(obj.optionalField === undefined || typeof obj.optionalField === 'number')
);
}
export function assertIsTypeName(value: unknown): asserts value is TypeName {
if (!isTypeName(value)) {
throw new TypeError(`Expected TypeName, got: ${JSON.stringify(value)}`);
}
}
Workflow
- Discovery: Run discovery commands to identify candidates across all packages.
- Selection: Choose a file or category with high-impact type issues.
- Create branch:
git checkout -b refactor/typescript-<area> - Fix types: Apply replacement strategies, starting with highest impact.
- Add type guards: Create reusable type guards for complex types.
- Validate: Run
pnpm typecheckandpnpm lintto ensure no regressions. - Run tests: Ensure all tests still pass.
- Commit and merge: Run
/commit-and-push, then/enter-merge-queue.
If no high-value fixes were found during discovery, do not create a branch or run commit/merge workflows.
Guardrails
- Do not change runtime behavior unless fixing a bug
- Do not introduce new
any,as, or@ts-ignore - Prefer gradual improvement over big-bang rewrites
- Keep PRs focused on one area or pattern
- Add tests for new type guards
- Document non-obvious type decisions with comments
Quality Bar
- Zero new
anytypes introduced - All type guards have corresponding tests
- No regression in type coverage
- All existing tests pass
- Lint and typecheck pass
PR Strategy
Use incremental PRs by category:
- PR 1: Fix
anyin shared types/interfaces - PR 2: Add type guards for API responses
- PR 3: Remove
ascasts in specific feature area - PR 4: Eliminate
@ts-ignorecomments
In each PR description, include:
- What category of type issues were fixed
- Files changed and why
- Any new type guards added
- Test evidence
Token Efficiency
Discovery commands can return many lines. Always limit output:
# Count first, then list limited results
rg -l ... | wc -l # Get count
rg -l ... | head -20 # Then sample
# Suppress verbose validation output
pnpm typecheck >/dev/null
pnpm lint >/dev/null
pnpm test >/dev/null
git commit -S -m "message" >/dev/null
git push >/dev/null
On failure, re-run without suppression to see errors.