Asked for simple types. Got a type system that would make category theorists weep. The journey from any
to academic dissertation in one LLM prompt.
"Add TypeScript types to this code." Simple request. The AI either responds with any
everywhere or creates a type system so complex that even the TypeScript compiler gives up and goes home.
There is no middle ground. Welcome to AI TypeScript gymnastics.
The Two Extremes
// Original JavaScript code
function calculateDiscount(user, product, coupon) {
let discount = 0;
if (user.isPremium) {
discount += 0.1;
}
if (product.category === 'electronics' && coupon) {
discount += coupon.value;
}
return Math.min(discount, 0.5);
}
// AI Approach #1: The "any" Apocalypse
function calculateDiscount(user: any, product: any, coupon: any): any {
let discount: any = 0;
if (user.isPremium) {
discount += 0.1;
}
if (product.category === 'electronics' && coupon) {
discount += coupon.value;
}
return Math.min(discount, 0.5);
}
// "There, it's TypeScript now!" 🤦
// AI Approach #2: The Type Olympian
type Nullable<T> = T | null | undefined;
type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T;
type Brand<K, T> = K & { __brand: T };
type UserId = Brand<string, 'UserId'>;
type ProductId = Brand<string, 'ProductId'>;
type Percentage = Brand<number, 'Percentage'>;
interface BaseEntity<T extends string> {
id: Brand<string, T>;
createdAt: Date;
updatedAt: Date;
metadata?: Record<string, unknown>;
}
interface User extends BaseEntity<'User'> {
isPremium: boolean;
subscriptionTier?: 'basic' | 'premium' | 'enterprise';
preferences?: DeepPartial<UserPreferences>;
}
interface UserPreferences {
notifications: NotificationPreferences;
privacy: PrivacySettings;
// ... 50 more nested interfaces
}
interface Product<T extends ProductCategory = ProductCategory> extends BaseEntity<'Product'> {
category: T;
pricing: PricingStrategy<T>;
}
type ProductCategory = 'electronics' | 'clothing' | 'food' | 'other';
interface PricingStrategy<T extends ProductCategory> {
basePrice: number;
rules: T extends 'electronics' ? ElectronicsPricingRules : GenericPricingRules;
}
interface Coupon<T extends DiscountType = DiscountType> extends BaseEntity<'Coupon'> {
type: T;
value: T extends 'percentage' ? Percentage : number;
restrictions?: CouponRestrictions<T>;
}
type DiscountType = 'percentage' | 'fixed' | 'bogo';
function calculateDiscount<
U extends User,
P extends Product,
C extends Nullable<Coupon>
>(
user: U,
product: P,
coupon: C
): C extends Coupon
? U['isPremium'] extends true
? P['category'] extends 'electronics'
? Percentage
: number
: number
: 0 {
// Implementation would be 100 lines of type assertions
return 0 as any; // Even AI gave up here
}
What We Actually Needed
// The sensible approach
interface User {
isPremium: boolean;
}
interface Product {
category: string;
}
interface Coupon {
value: number;
}
function calculateDiscount(
user: User,
product: Product,
coupon?: Coupon
): number {
let discount = 0;
if (user.isPremium) {
discount += 0.1;
}
if (product.category === 'electronics' && coupon) {
discount += coupon.value;
}
return Math.min(discount, 0.5);
}
The React Component Disaster
// Original simple component
function UserCard({ user, onEdit }) {
return (
<div onClick={() => onEdit(user.id)}>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
}
// AI's type explosion
type HTMLDivElementProps = React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>, HTMLDivElement
>;
interface UserCardProps extends Omit<HTMLDivElementProps, 'onClick'> {
user: Readonly<{
id: UserId;
name: string;
email: Email;
profile?: DeepPartial<UserProfile>;
}>;
onEdit: (id: UserId) => void | Promise<void>;
children?: never; // AI decided no children allowed
className?: string; // But wait, this is already in HTMLDivElementProps...
style?: React.CSSProperties; // So is this...
}
const UserCard: React.FC<UserCardProps> = React.memo(({
user,
onEdit,
...restProps
}) => {
const handleClick = React.useCallback(
(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
event.preventDefault();
event.stopPropagation();
void Promise.resolve(onEdit(user.id));
},
[user.id, onEdit]
);
return (
<div {...restProps} onClick={handleClick}>
{/* AI added null checks for required fields */}
<h3>{user?.name ?? 'Unknown User'}</h3>
<p>{user?.email ?? 'No email'}</p>
</div>
);
});
UserCard.displayName = 'UserCard';
The API Response Nightmare
// Simple fetch function
async function getUser(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
// AI's "helpful" types
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
type APIEndpoint<T extends string> = `/api/${T}`;
type ResourceIdentifier<T extends string> = Brand<string, T>;
interface APIResponse<T, E extends APIError = APIError> {
data?: T;
error?: E;
meta: ResponseMetadata;
}
interface ResponseMetadata {
timestamp: number;
requestId: string;
version: string;
rateLimit: RateLimitInfo;
}
interface RateLimitInfo {
limit: number;
remaining: number;
reset: Date;
}
abstract class APIError extends Error {
abstract readonly code: string;
abstract readonly statusCode: number;
abstract readonly retryable: boolean;
}
class UserNotFoundError extends APIError {
readonly code = 'USER_NOT_FOUND';
readonly statusCode = 404;
readonly retryable = false;
}
class NetworkError extends APIError {
readonly code = 'NETWORK_ERROR';
readonly statusCode = 0;
readonly retryable = true;
}
class ValidationError extends APIError {
readonly code = 'VALIDATION_ERROR';
readonly statusCode = 400;
readonly retryable = false;
constructor(public readonly errors: Record<string, string[]>) {
super('Validation failed');
}
}
// ... 20 more error classes
async function getUser<T extends ResourceIdentifier<'User'>>(
id: T
): Promise<APIResponse<User, UserNotFoundError | NetworkError | ValidationError>> {
// AI somehow made a simple fetch into a distributed systems problem
const endpoint: APIEndpoint<'users'> = `/api/users`;
const response = await fetch(`${endpoint}/${id}`, {
method: 'GET' as HTTPMethod,
headers: {
'Content-Type': 'application/json',
'X-Request-ID': crypto.randomUUID(),
},
});
// 50 more lines of type gymnastics...
return {} as any;
}
The Form Validation Fiasco
// Original validation
function validateEmail(email) {
return email.includes('@');
}
// AI's enterprise-grade solution
type EmailLocalPart = Brand<string, 'EmailLocalPart'>;
type EmailDomain = Brand<string, 'EmailDomain'>;
type ValidatedEmail = Brand<string, 'ValidatedEmail'>;
interface EmailValidationResult<T extends string> {
isValid: T extends ValidatedEmail ? true : false;
localPart?: T extends ValidatedEmail ? EmailLocalPart : never;
domain?: T extends ValidatedEmail ? EmailDomain : never;
errors?: T extends ValidatedEmail ? never : EmailValidationError[];
}
enum EmailValidationErrorCode {
MISSING_AT_SYMBOL = 'MISSING_AT_SYMBOL',
INVALID_LOCAL_PART = 'INVALID_LOCAL_PART',
INVALID_DOMAIN = 'INVALID_DOMAIN',
CONSECUTIVE_DOTS = 'CONSECUTIVE_DOTS',
STARTS_WITH_DOT = 'STARTS_WITH_DOT',
ENDS_WITH_DOT = 'ENDS_WITH_DOT',
// ... 30 more error codes
}
interface EmailValidationError {
code: EmailValidationErrorCode;
message: string;
position?: number;
suggestion?: string;
}
function validateEmail<T extends string>(
email: T
): EmailValidationResult<T> {
// 200 lines of validation logic
// Still just checks for '@' in the end
}
The State Management Monstrosity
// Simple state
const [user, setUser] = useState({ name: '', age: 0 });
// AI's Redux-on-steroids approach
type Action<T extends string, P = void> = P extends void
? { type: T }
: { type: T; payload: P };
type UserAction =
| Action<'SET_NAME', string>
| Action<'SET_AGE', number>
| Action<'INCREMENT_AGE'>
| Action<'DECREMENT_AGE'>
| Action<'RESET'>
| Action<'HYDRATE', User>
| Action<'UPDATE', Partial<User>>;
interface UserState {
current: User;
previous: User[];
future: User[];
isDirty: boolean;
lastModified: Date;
modificationCount: number;
}
const userReducer: Reducer<UserState, UserAction> = (state, action) => {
// 100 lines of reducer logic for 2 fields
};
const useUser = () => {
const [state, dispatch] = useReducer(userReducer, initialState);
const setName = useCallback((name: string) => {
dispatch({ type: 'SET_NAME', payload: name });
}, []);
const setAge = useCallback((age: number) => {
dispatch({ type: 'SET_AGE', payload: age });
}, []);
// 20 more action creators...
return {
user: state.current,
setName,
setAge,
// ... all the things
};
};
When AI Types Make Sense
// Good use case: Complex data transformations
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
// Good use case: API response typing
interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
pageSize: number;
hasNext: boolean;
hasPrevious: boolean;
}
// Good use case: Event handling
type EventHandler<T = Event> = (event: T) => void | Promise<void>;
// Good use case: Generic constraints
function updateArray<T extends { id: string }>(
items: T[],
id: string,
updates: Partial<T>
): T[] {
return items.map(item =>
item.id === id ? { ...item, ...updates } : item
);
}
The Balance We Need
// Start with simple types
interface User {
id: string;
name: string;
email: string;
isPremium: boolean;
}
// Add complexity only when needed
interface DetailedUser extends User {
profile: UserProfile;
settings: UserSettings;
subscription?: Subscription;
}
// Use utility types wisely
type PublicUser = Pick<User, 'id' | 'name'>;
type UserUpdate = Partial<Omit<User, 'id'>>;
// Keep functions readable
function createUser(data: Omit<User, 'id'>): User {
return {
id: generateId(),
...data
};
}
Lessons Learned
- Start simple - You can always add complexity
- Avoid type gymnastics - If it takes 5 minutes to understand, it's too complex
- Use built-in utility types - Partial, Pick, Omit are your friends
- Type what matters - Not everything needs generics
- Remember the human - Someone has to maintain this
The Golden Rules
// ❌ Bad: Over-engineered
type Maybe<T> = T | null | undefined;
type Nullable<T> = T | null;
type Optional<T> = T | undefined;
type Perhaps<T> = Maybe<T>; // Why?
// ✅ Good: Use what TypeScript gives you
type UserInput = {
name?: string; // optional
email: string | null; // nullable
age: number | undefined; // explicit undefined
};
// ❌ Bad: Unnecessary abstraction
interface IUserService {
getUser<T extends UserId>(id: T): Promise<User>;
}
class UserService implements IUserService {
async getUser<T extends UserId>(id: T): Promise<User> {
// ...
}
}
// ✅ Good: Direct and clear
class UserService {
async getUser(id: string): Promise<User> {
// ...
}
}
After a month of watching AI write TypeScript types, I've learned that the best types are the ones that help developers, not impress them. AI tends to forget this simple truth.
The irony? The same AI that creates these type monstrosities can't even figure out how to use them properly in the implementation. It's like asking a master chef to cook, and they respond by inventing a new periodic table of flavors instead of making dinner.
Keep it simple. Your future self will thank you.