Investigate how AI sometimes generates JavaScript when TypeScript is appropriate, creates overly permissive types using any everywhere, or produces incorrect type definitions that compile but break at runtime. Learn strict TypeScript configuration, type guards, branded types, and utility types.
Introduction: The Type Safety Crisis in AI-Generated Code
TypeScript was created to add static typing to JavaScript, catching errors at compile time rather than runtime. Yet when AI generates code for TypeScript projects, it often undermines these benefits by using any liberally, ignoring strict mode settings, and producing types that pass the compiler but fail in production.
Key Statistics
- 40-60% of AI-generated TypeScript contains unnecessary
anytypes - 15% type error reduction with strict mode enabled
- 3x fewer runtime errors in strict TypeScript codebases
- 97% of JavaScript projects can benefit from TypeScript adoption
The fundamental problem is that AI optimizes for code that compiles, not code that's type-safe. Using any is the fastest way to silence type errors, and AI training data is full of such shortcuts. This article explores the common type safety failures in AI-generated code and provides comprehensive solutions.
Why AI Fails at TypeScript
Training Data Problems
AI models learn from public repositories where type safety practices vary wildly:
- Tutorial code uses
any: Examples prioritize simplicity over safety - Legacy migrations: Many codebases have
anyfrom JS-to-TS migrations - Quick fixes: Stack Overflow answers often use
anyto "solve" type errors - Non-strict configurations: Many projects don't enable strict mode
- Outdated patterns: Training data includes pre-TypeScript 4.x patterns
Common AI TypeScript Mistakes
- Generating JavaScript: AI generates
.jsfiles in TypeScript projects - Overusing
any: Type errors silenced withanyinstead of proper types - Ignoring generics: Using concrete types where generics would be reusable
- Missing null checks: Assuming values exist without optional chaining
- Type assertions abuse: Using
as Typeto force incorrect types - Incorrect inference: Not helping TypeScript infer complex types
The any Problem: Type Safety Killer
The any type effectively disables TypeScript's type checking for a value. Once a value is typed as any, it propagates through your codebase, infecting everything it touches.
// AI-Generated: Overly permissive types with 'any' everywhere
async function fetchUserData(userId: any): Promise<any> {
const response = await fetch(`/api/users/${userId}`);
const data: any = await response.json();
return data;
}
function processUser(user: any) {
// No type safety at all - any property access is allowed
console.log(user.naem); // Typo not caught!
console.log(user.email.toUppercase()); // Another typo!
// No IntelliSense, no autocomplete, no safety
return {
fullName: user.firstName + ' ' + user.lastName,
age: user.age,
isAdmin: user.role === 'admin'
};
}
// This compiles but crashes at runtime
const result = processUser({ name: 'John' });
console.log(result.fullName); // "undefined undefined"
// Proper: Fully typed with interfaces
interface User {
id: string;
firstName: string;
lastName: string;
email: string;
age: number;
role: 'user' | 'admin' | 'moderator';
createdAt: Date;
}
interface ApiResponse<T> {
data: T;
status: number;
message?: string;
}
async function fetchUserData(userId: string): Promise<User> {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`Failed to fetch user: ${response.status}`);
}
const json: ApiResponse<User> = await response.json();
return json.data;
}
function processUser(user: User) {
// TypeScript catches typos immediately
// console.log(user.naem); // Error: Property 'naem' does not exist
// console.log(user.email.toUppercase()); // Error: Did you mean 'toUpperCase'?
return {
fullName: `${user.firstName} ${user.lastName}`,
age: user.age,
isAdmin: user.role === 'admin'
};
}
// This won't compile - missing required properties
// const result = processUser({ name: 'John' }); // Error!
Strict TypeScript Configuration
The most effective defense against AI's type laxity is a strict TypeScript configuration. Strict mode enables multiple compiler checks that catch common errors.
Recommended tsconfig.json
{
"compilerOptions": {
// Enable all strict checks
"strict": true,
// Additional strict checks not in "strict"
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noPropertyAccessFromIndexSignature": true,
// Catch unused code
"noUnusedLocals": true,
"noUnusedParameters": true,
// Prevent implicit any in catch clauses
"useUnknownInCatchVariables": true,
// Ensure consistency
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
// Module settings
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"isolatedModules": true,
// Output settings
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
// Declaration files for libraries
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
What Each Strict Option Does
strict: trueenables: strictNullChecks, noImplicitAny, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, alwaysStrict, useUnknownInCatchVariablesnoUncheckedIndexedAccess: Array/object index access returnsT | undefined, forcing null checksexactOptionalPropertyTypes: Distinguishes between missing property and property set to undefinednoPropertyAccessFromIndexSignature: Forces bracket notation for index signature access
Impact of noUncheckedIndexedAccess
// Without noUncheckedIndexedAccess
const users: User[] = [];
const firstUser = users[0]; // Type: User (dangerous!)
console.log(firstUser.name); // Runtime error: Cannot read property of undefined
// With noUncheckedIndexedAccess
const users: User[] = [];
const firstUser = users[0]; // Type: User | undefined (safe!)
// console.log(firstUser.name); // Error: Object is possibly undefined
// Must check first
if (firstUser) {
console.log(firstUser.name); // Safe
}
// Or use optional chaining
console.log(firstUser?.name); // Safe, returns undefined
Type Guards: Runtime Type Safety
Type guards are functions that perform runtime checks and inform TypeScript about the narrowed type. They're essential when working with external data (APIs, user input) where compile-time types aren't guaranteed.
Basic Type Guards
// typeof type guard
function processValue(value: unknown): string {
if (typeof value === 'string') {
return value.toUpperCase(); // TypeScript knows it's a string
}
if (typeof value === 'number') {
return value.toFixed(2); // TypeScript knows it's a number
}
throw new Error('Unsupported type');
}
// instanceof type guard
class ApiError extends Error {
constructor(public statusCode: number, message: string) {
super(message);
}
}
function handleError(error: unknown): void {
if (error instanceof ApiError) {
console.log(`API Error ${error.statusCode}: ${error.message}`);
} else if (error instanceof Error) {
console.log(`Error: ${error.message}`);
} else {
console.log('Unknown error:', error);
}
}
// 'in' operator type guard
interface Dog {
bark(): void;
breed: string;
}
interface Cat {
meow(): void;
color: string;
}
function makeSound(animal: Dog | Cat): void {
if ('bark' in animal) {
animal.bark(); // TypeScript knows it's a Dog
} else {
animal.meow(); // TypeScript knows it's a Cat
}
}
Custom Type Guard Functions
// User-defined type guard with 'is' keyword
interface User {
id: string;
name: string;
email: string;
}
// Type guard function
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
'email' in value &&
typeof (value as User).id === 'string' &&
typeof (value as User).name === 'string' &&
typeof (value as User).email === 'string'
);
}
// Usage
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data: unknown = await response.json();
if (!isUser(data)) {
throw new Error('Invalid user data received from API');
}
// TypeScript now knows data is User
return data;
}
// Assertion function (throws if not valid)
function assertIsUser(value: unknown): asserts value is User {
if (!isUser(value)) {
throw new Error('Value is not a valid User');
}
}
// Usage with assertion
function processUserData(data: unknown): string {
assertIsUser(data); // Throws if invalid
// After this line, TypeScript knows data is User
return `${data.name} (${data.email})`;
}
Type Guards with Zod (Schema Validation)
import { z } from 'zod';
// Define schema
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().positive().optional(),
role: z.enum(['user', 'admin', 'moderator']),
createdAt: z.string().datetime().transform(s => new Date(s))
});
// Infer TypeScript type from schema
type User = z.infer<typeof UserSchema>;
// Type guard using Zod
function isUser(value: unknown): value is User {
return UserSchema.safeParse(value).success;
}
// Parse with error details
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
const result = UserSchema.safeParse(data);
if (!result.success) {
console.error('Validation errors:', result.error.issues);
throw new Error('Invalid user data');
}
return result.data; // Fully typed User
}
// Partial schema for updates
const UserUpdateSchema = UserSchema.partial().omit({ id: true, createdAt: true });
type UserUpdate = z.infer<typeof UserUpdateSchema>;
Branded Types: Nominal Typing in TypeScript
TypeScript uses structural typing, meaning any object with the right shape is compatible. This can cause bugs when semantically different values have the same structure. Branded types (also called opaque types) add nominal typing.
The Problem with Structural Typing
// Without branded types - dangerous!
type UserId = string;
type OrderId = string;
type ProductId = string;
function getUser(id: UserId): Promise<User> { /* ... */ }
function getOrder(id: OrderId): Promise<Order> { /* ... */ }
const userId: UserId = 'user-123';
const orderId: OrderId = 'order-456';
// These compile but are logically wrong!
getUser(orderId); // No error - orderId is a string!
getOrder(userId); // No error - userId is a string!
Implementing Branded Types
// Branded type pattern
declare const brand: unique symbol;
type Brand<T, B> = T & { readonly [brand]: B };
// Create branded types
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
type ProductId = Brand<string, 'ProductId'>;
// Type-safe constructors
function createUserId(id: string): UserId {
// Add validation if needed
if (!id.startsWith('user-')) {
throw new Error('Invalid user ID format');
}
return id as UserId;
}
function createOrderId(id: string): OrderId {
if (!id.startsWith('order-')) {
throw new Error('Invalid order ID format');
}
return id as OrderId;
}
// Now these are type-safe
function getUser(id: UserId): Promise<User> { /* ... */ }
function getOrder(id: OrderId): Promise<Order> { /* ... */ }
const userId = createUserId('user-123');
const orderId = createOrderId('order-456');
getUser(userId); // OK
// getUser(orderId); // Error: Argument of type 'OrderId' is not assignable to 'UserId'
// getOrder(userId); // Error: Argument of type 'UserId' is not assignable to 'OrderId'
Branded Types for Validated Data
// Branded types for validated strings
type Email = Brand<string, 'Email'>;
type URL = Brand<string, 'URL'>;
type NonEmptyString = Brand<string, 'NonEmptyString'>;
type PositiveNumber = Brand<number, 'PositiveNumber'>;
// Validation constructors
function validateEmail(value: string): Email {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
throw new Error(`Invalid email: ${value}`);
}
return value as Email;
}
function validatePositive(value: number): PositiveNumber {
if (value <= 0) {
throw new Error(`Value must be positive: ${value}`);
}
return value as PositiveNumber;
}
// Functions that require validated input
function sendEmail(to: Email, subject: string, body: string): void {
// We know 'to' is a valid email - no need to validate again
console.log(`Sending email to ${to}`);
}
function setQuantity(quantity: PositiveNumber): void {
// We know quantity is positive - no need to check again
console.log(`Setting quantity to ${quantity}`);
}
// Usage
const email = validateEmail('user@example.com');
const quantity = validatePositive(5);
sendEmail(email, 'Hello', 'World'); // OK
// sendEmail('maybe-invalid', 'Hello', 'World'); // Error!
Utility Types: Leveraging TypeScript's Power
TypeScript provides built-in utility types that transform existing types. AI often ignores these, creating verbose or incorrect types manually.
Essential Utility Types
interface User {
id: string;
name: string;
email: string;
age: number;
role: 'user' | 'admin';
createdAt: Date;
updatedAt: Date;
}
// Partial<T> - All properties optional
type UserUpdate = Partial<User>;
// { id?: string; name?: string; email?: string; ... }
// Required<T> - All properties required
type RequiredUser = Required<Partial<User>>;
// Readonly<T> - All properties readonly
type ImmutableUser = Readonly<User>;
// const user: ImmutableUser = { ... };
// user.name = 'new'; // Error: Cannot assign to 'name'
// Pick<T, K> - Select specific properties
type UserPreview = Pick<User, 'id' | 'name' | 'email'>;
// { id: string; name: string; email: string; }
// Omit<T, K> - Exclude specific properties
type UserWithoutTimestamps = Omit<User, 'createdAt' | 'updatedAt'>;
// { id: string; name: string; email: string; age: number; role: ... }
// Record<K, V> - Create object type with key type and value type
type UserRoles = Record<string, 'user' | 'admin' | 'moderator'>;
// { [key: string]: 'user' | 'admin' | 'moderator' }
// Extract<T, U> - Extract types from union
type StringOrNumber = string | number | boolean;
type OnlyStrings = Extract<StringOrNumber, string>; // string
// Exclude<T, U> - Exclude types from union
type NotString = Exclude<StringOrNumber, string>; // number | boolean
// NonNullable<T> - Remove null and undefined
type MaybeUser = User | null | undefined;
type DefinitelyUser = NonNullable<MaybeUser>; // User
Advanced Utility Type Patterns
// ReturnType<T> - Get function return type
function createUser(name: string, email: string) {
return { id: crypto.randomUUID(), name, email, createdAt: new Date() };
}
type CreatedUser = ReturnType<typeof createUser>;
// Parameters<T> - Get function parameter types as tuple
type CreateUserParams = Parameters<typeof createUser>;
// [name: string, email: string]
// Awaited<T> - Unwrap Promise type
type AsyncUser = Promise<User>;
type ResolvedUser = Awaited<AsyncUser>; // User
// InstanceType<T> - Get instance type from constructor
class UserService {
getUser(id: string): User { /* ... */ }
}
type UserServiceInstance = InstanceType<typeof UserService>;
// Custom utility types
// DeepPartial - Make all nested properties optional
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// DeepReadonly - Make all nested properties readonly
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
// RequireAtLeastOne - At least one property must be present
type RequireAtLeastOne<T, Keys extends keyof T = keyof T> =
Pick<T, Exclude<keyof T, Keys>> &
{ [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>> }[Keys];
// Usage: search must have at least one of: query, filters, or category
interface SearchParams {
query?: string;
filters?: string[];
category?: string;
}
type ValidSearch = RequireAtLeastOne<SearchParams, 'query' | 'filters' | 'category'>;
Generic Type Patterns
AI often uses concrete types where generics would make code more reusable. Understanding generic patterns is essential for type-safe, DRY code.
// AI-Generated: Duplicated code for each type
function getFirstUser(users: User[]): User | undefined {
return users[0];
}
function getFirstProduct(products: Product[]): Product | undefined {
return products[0];
}
function getFirstOrder(orders: Order[]): Order | undefined {
return orders[0];
}
// Proper: Generic function
function getFirst<T>(items: T[]): T | undefined {
return items[0];
}
// TypeScript infers the type
const user = getFirst(users); // User | undefined
const product = getFirst(products); // Product | undefined
// Generic with constraints
interface HasId {
id: string;
}
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
// Generic with multiple type parameters
function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
const merged = merge({ name: 'John' }, { age: 30 });
// Type: { name: string } & { age: number }
// Generic class
class Repository<T extends HasId> {
private items: Map<string, T> = new Map();
add(item: T): void {
this.items.set(item.id, item);
}
get(id: string): T | undefined {
return this.items.get(id);
}
getAll(): T[] {
return Array.from(this.items.values());
}
delete(id: string): boolean {
return this.items.delete(id);
}
}
// Usage
const userRepo = new Repository<User>();
userRepo.add({ id: '1', name: 'John', email: 'john@example.com' });
const user = userRepo.get('1'); // User | undefined
Type Coverage Tools
Measuring and enforcing type coverage helps maintain type safety as your codebase grows. These tools catch AI-generated code that bypasses TypeScript's protection.
Using type-coverage
# Install type-coverage
npm install -D type-coverage
# Run type coverage check
npx type-coverage
# Output:
# 95.42% (1234/1293)
# Strict mode (fails if below threshold)
npx type-coverage --at-least 95
# Show uncovered lines
npx type-coverage --detail
# Ignore catch clause variables
npx type-coverage --ignore-catch
ESLint TypeScript Rules
// .eslintrc.js
module.exports = {
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:@typescript-eslint/strict'
],
parserOptions: {
project: './tsconfig.json'
},
rules: {
// Disallow any type
'@typescript-eslint/no-explicit-any': 'error',
// Require explicit return types on functions
'@typescript-eslint/explicit-function-return-type': 'warn',
// Disallow non-null assertions
'@typescript-eslint/no-non-null-assertion': 'error',
// Require type annotations in certain places
'@typescript-eslint/typedef': ['error', {
'arrowParameter': true,
'memberVariableDeclaration': true,
'parameter': true,
'propertyDeclaration': true
}],
// Disallow unsafe member access on any
'@typescript-eslint/no-unsafe-member-access': 'error',
// Disallow calling functions typed as any
'@typescript-eslint/no-unsafe-call': 'error',
// Disallow returning any from functions
'@typescript-eslint/no-unsafe-return': 'error',
// Prefer nullish coalescing
'@typescript-eslint/prefer-nullish-coalescing': 'error',
// Prefer optional chaining
'@typescript-eslint/prefer-optional-chain': 'error'
}
};
JavaScript to TypeScript Migration
When AI generates JavaScript in a TypeScript project, or when migrating an existing codebase, follow these strategies for gradual, safe migration.
Migration Configuration
// tsconfig.json for migration
{
"compilerOptions": {
// Allow JavaScript files
"allowJs": true,
// Check JavaScript files for errors
"checkJs": true,
// Start with loose settings
"strict": false,
"noImplicitAny": false,
// Enable incrementally
"strictNullChecks": true, // Enable first - high impact
// Output alongside source
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"]
}
Gradual Strict Mode Adoption
// Phase 1: Enable in specific files with directive
// At top of file: // @ts-strict
// Phase 2: Enable strictNullChecks project-wide
// This catches the most bugs
// Phase 3: Enable noImplicitAny
// Forces explicit types where inference fails
// Phase 4: Enable strict: true
// Full type safety
// Use @ts-expect-error for known issues during migration
function legacyFunction(data) {
// @ts-expect-error - TODO: Add proper types in JIRA-123
return data.process();
}
Type Declaration Files for Legacy Code
// legacy-module.d.ts
// Declare types for untyped JavaScript modules
declare module 'legacy-module' {
export interface LegacyConfig {
apiKey: string;
endpoint: string;
timeout?: number;
}
export function initialize(config: LegacyConfig): void;
export function fetchData<T>(path: string): Promise<T>;
export class LegacyClient {
constructor(config: LegacyConfig);
get<T>(path: string): Promise<T>;
post<T, D>(path: string, data: D): Promise<T>;
}
export default LegacyClient;
}
Key Takeaways
Remember These Points
- Enable strict mode: Set
strict: truein tsconfig.json and addnoUncheckedIndexedAccess - Ban
any: Use ESLint'sno-explicit-anyrule to catch AI-generated any types - Use type guards: Validate external data at runtime with type guard functions
- Implement branded types: Prevent mixing semantically different values (UserId vs OrderId)
- Leverage utility types: Use Partial, Pick, Omit, Record instead of manual type definitions
- Write generics: Create reusable, type-safe functions instead of duplicating for each type
- Validate with Zod: Schema validation libraries provide both runtime safety and TypeScript types
- Measure coverage: Use type-coverage to track and enforce type safety thresholds
Conclusion
Type safety is one of TypeScript's primary benefits, yet AI code generation frequently undermines it. By using any liberally and ignoring strict mode settings, AI-generated code compiles without errors but fails at runtime—exactly what TypeScript was designed to prevent.
The solution requires a multi-layered approach: strict compiler settings that reject unsafe code, ESLint rules that catch type safety violations, type guards that validate external data at runtime, and branded types that prevent logical errors. These practices ensure that AI-generated code meets the same type safety standards as hand-written code.
Remember that TypeScript is only as strong as your configuration allows. A loose configuration with AI-generated code full of any types provides no more safety than plain JavaScript. By enforcing strict settings and measuring type coverage, you can maintain the productivity benefits of AI assistance while preserving the reliability guarantees that make TypeScript valuable.
The goal isn't to reject AI-generated code—it's to transform it into production-quality TypeScript that catches errors at compile time rather than in production.