| name | typescript-strict-migrator |
| description | Migrates TypeScript projects to strict mode incrementally with type guards, utility types, and best practices. Use when users request "TypeScript strict", "strict mode migration", "type safety", "strict TypeScript", or "ts-strict". |
TypeScript Strict Migrator
Incrementally migrate to TypeScript strict mode for maximum type safety.
Core Workflow
- Audit current state: Check existing type errors
- Enable incrementally: One flag at a time
- Fix errors: Systematic approach per flag
- Add type guards: Runtime type checking
- Use utility types: Proper type transformations
- Document patterns: Team guidelines
Strict Mode Flags
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"useUnknownInCatchVariables": true,
"alwaysStrict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true,
"noPropertyAccessFromIndexSignature": true
}
}
Incremental Migration Strategy
Phase 1: Basic Strict Flags
{
"compilerOptions": {
"strict": false,
"noImplicitAny": true,
"alwaysStrict": true
}
}
function processData(data) {
return data.map(item => item.value);
}
function processData(data: DataItem[]): number[] {
return data.map(item => item.value);
}
interface DataItem {
value: number;
label: string;
}
Phase 2: Strict Null Checks
{
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true
}
}
function getUserName(user: User) {
return user.profile.name;
}
function getUserName(user: User): string | undefined {
return user.profile?.name;
}
function getUserNameOrThrow(user: User): string {
if (!user.profile?.name) {
throw new Error('User has no name');
}
return user.profile.name;
}
Phase 3: Function Types
{
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true
}
}
type Handler = (event: Event) => void;
const mouseHandler: Handler = (event: MouseEvent) => {
console.log(event.clientX);
};
type Handler<T extends Event = Event> = (event: T) => void;
const mouseHandler: Handler<MouseEvent> = (event) => {
console.log(event.clientX);
};
Phase 4: Property Initialization
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true
}
}
class UserService {
private apiClient: ApiClient;
constructor() {}
}
class UserService {
private apiClient: ApiClient;
constructor(apiClient: ApiClient) {
this.apiClient = apiClient;
}
}
class UserService {
private apiClient!: ApiClient;
async init() {
this.apiClient = await createApiClient();
}
}
Type Guards
Basic Type Guards
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isNumber(value: unknown): value is number {
return typeof value === 'number';
}
function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
function isArray<T>(value: unknown, itemGuard: (item: unknown) => item is T): value is T[] {
return Array.isArray(value) && value.every(itemGuard);
}
function processInput(input: unknown) {
if (isString(input)) {
return input.toUpperCase();
}
if (isNumber(input)) {
return input.toFixed(2);
}
throw new Error('Invalid input type');
}
Object Type Guards
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}
interface ApiResponse<T> {
data: T;
success: boolean;
}
function isUser(value: unknown): value is User {
return (
isObject(value) &&
typeof value.id === 'string' &&
typeof value.name === 'string' &&
typeof value.email === 'string' &&
(value.role === 'admin' || value.role === 'user')
);
}
function isApiResponse<T>(
value: unknown,
dataGuard: (data: unknown) => data is T
): value is ApiResponse<T> {
return (
isObject(value) &&
typeof value.success === 'boolean' &&
'data' in value &&
dataGuard(value.data)
);
}
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data: unknown = await response.json();
if (!isApiResponse(data, isUser)) {
throw new Error('Invalid API response');
}
return data.data;
}
Discriminated Unions
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
function createSuccess<T>(data: T): Result<T> {
return { success: true, data };
}
function createError<E = Error>(error: E): Result<never, E> {
return { success: false, error };
}
function isSuccess<T, E>(result: Result<T, E>): result is { success: true; data: T } {
return result.success === true;
}
async function processRequest(): Promise<Result<User>> {
try {
const user = await fetchUser('123');
return createSuccess(user);
} catch (error) {
return createError(error instanceof Error ? error : new Error(String(error)));
}
}
const result = await processRequest();
if (isSuccess(result)) {
console.log(result.data.name);
} else {
console.error(result.error.message);
}
Utility Types for Migration
type RequiredUser = Required<User>;
type PartialUser = Partial<User>;
type UserCredentials = Pick<User, 'email' | 'id'>;
type PublicUser = Omit<User, 'password' | 'internalId'>;
type ReadonlyUser = Readonly<User>;
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]>
: T[P];
};
type DefiniteString = NonNullable<string | null | undefined>;
type AdminRole = Extract<User['role'], 'admin'>;
type NonAdminRole = Exclude<User['role'], 'admin'>;
type UserById = Record<string, User>;
type FetchParams = Parameters<typeof fetch>;
type FetchReturn = ReturnType<typeof fetch>;
Common Migration Patterns
Handling Optional Chaining
const userName = user.profile.settings.displayName;
const userName = user?.profile?.settings?.displayName;
const userName = user?.profile?.settings?.displayName ?? 'Anonymous';
function getDisplayName(user: User | null): string {
if (!user?.profile?.settings?.displayName) {
return 'Anonymous';
}
return user.profile.settings.displayName;
}
Assertion Functions
function assertIsDefined<T>(value: T): asserts value is NonNullable<T> {
if (value === undefined || value === null) {
throw new Error('Value is not defined');
}
}
function assertIsUser(value: unknown): asserts value is User {
if (!isUser(value)) {
throw new Error('Value is not a User');
}
}
function processUser(maybeUser: unknown) {
assertIsUser(maybeUser);
console.log(maybeUser.name);
}
Error Handling
try {
await riskyOperation();
} catch (error) {
console.error(error.message);
}
try {
await riskyOperation();
} catch (error) {
if (error instanceof Error) {
console.error(error.message);
} else {
console.error('Unknown error:', String(error));
}
}
function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
if (typeof error === 'string') return error;
return 'Unknown error occurred';
}
Index Signatures
const users: Record<string, User> = {};
const user = users['unknown-id'];
console.log(user.name);
const user = users['unknown-id'];
if (user) {
console.log(user.name);
}
const user = users['known-id']!;
const usersMap = new Map<string, User>();
const user = usersMap.get('some-id');
Migration Script
import * as ts from 'typescript';
import * as path from 'path';
interface StrictAnalysis {
noImplicitAny: number;
strictNullChecks: number;
strictFunctionTypes: number;
strictPropertyInitialization: number;
total: number;
}
function analyzeProject(configPath: string): StrictAnalysis {
const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
const parsedConfig = ts.parseJsonConfigFileContent(
configFile.config,
ts.sys,
path.dirname(configPath)
);
const strictOptions = {
noImplicitAny: true,
strictNullChecks: true,
strictFunctionTypes: true,
strictPropertyInitialization: true,
};
const analysis: StrictAnalysis = {
noImplicitAny: 0,
strictNullChecks: 0,
strictFunctionTypes: 0,
strictPropertyInitialization: 0,
total: 0,
};
for (const [flag, _] of Object.entries(strictOptions)) {
const options = {
...parsedConfig.options,
[flag]: true,
};
const program = ts.createProgram(parsedConfig.fileNames, options);
const diagnostics = ts.getPreEmitDiagnostics(program);
analysis[flag as keyof StrictAnalysis] = diagnostics.length;
analysis.total += diagnostics.length;
}
return analysis;
}
const analysis = analyzeProject('./tsconfig.json');
console.log('Strict mode analysis:', analysis);
Best Practices
- Incremental adoption: One flag at a time
- Start with noImplicitAny: Easiest to fix
- Add type guards: Runtime safety
- Use assertion functions: Fail fast
- Avoid non-null assertions: Use sparingly
- Document patterns: Team consistency
- CI enforcement: Prevent regression
- Use unknown over any: Better type safety
Output Checklist
Every strict migration should include: