TypeScript adoption among Indian developers accelerated sharply through 2023–2025 as React, Next.js, and Node.js began defaulting to TypeScript in their official project templates. Hiring managers at Indian product companies — from Zoho and Freshworks to early-stage Bangalore startups — now scan for TypeScript fluency as a baseline signal. But widespread adoption produced a revealing anti-pattern: JavaScript written with TypeScript syntax. Developers use any liberally, cast values with as whenever the compiler protests, and leave tsconfig.json at permissive defaults that the TypeScript team itself discourages. The result is code that wears the TypeScript label without gaining its benefits.
Genuine TypeScript expertise — the kind that commands ₹18–35 lakh salaries at Indian product companies — requires understanding the type system deeply enough to use it as a design tool. The compiler becomes a collaborator that catches entire categories of bugs before a single test runs. This post covers the configurations, patterns, and mental models that separate that level of proficiency from surface-level adoption.
tsconfig.json — Start with Strict Mode
The single highest-leverage change in any TypeScript project is enabling "strict": true in compilerOptions. This one flag activates a bundle of checks simultaneously: strictNullChecks, noImplicitAny, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, and useUnknownInCatchVariables. Each individually closes a gap that silent JavaScript failures exploit.
Beyond the strict bundle, three additional options belong in serious production TypeScript configurations:
"noUncheckedIndexedAccess": true— When you accessarray[index], TypeScript normally infers the type asT. With this option enabled, it infersT | undefined, reflecting the reality that any index might be out of bounds. This flag alone prevents an entire class of silent runtime failures."exactOptionalPropertyTypes": true— TypeScript normally treats an absent optional property and a property explicitly set toundefinedas equivalent. This option distinguishes them, which matters when you merge partial updates into records or spread objects."noImplicitReturns": true— Requires every code path through a function to return a value. Without this, a function returningstringcan silently returnundefinedthrough an uncovered conditional branch.
Enabling strict mode on an existing project surfaces hundreds of latent type errors that were previously silently accepted. Treat this as a discovery phase — it reveals what the codebase was already doing wrong, not something the compiler introduced. Migrate incrementally by suppressing individual errors with // @ts-expect-error and a comment describing what needs fixing, rather than reaching for any.
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"skipLibCheck": true,
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}
Avoiding any — The Type System Escape Hatch
The any type is a complete opt-out from TypeScript's type checking. What makes it particularly dangerous is that it is infectious — assigning an any-typed value to a typed variable silently coerces that variable to any, spreading the escape hatch through the codebase. Enabling "noImplicitAny": true (included in strict) blocks implicit any, but explicit any remains your responsibility to avoid.
Two alternatives cover most legitimate any use cases:
unknown is the type-safe counterpart to any. A value typed as unknown cannot be used without first narrowing it to a more specific type — via a typeof check, an instanceof check, or a custom type guard. This is exactly the correct behaviour for values from external API responses, JSON.parse() output, user-supplied input, or caught errors. The compiler forces you to handle the uncertainty explicitly rather than pretending the value is whatever type you wish it were.
never represents a value that should logically never occur. Its primary production use is exhaustive checks in switch statements. If a switch handles all members of a discriminated union and assigns the default case to a never variable, adding a new union member without updating the switch produces a compile error — catching the omission at build time rather than in production:
type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; side: number };
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle': return Math.PI * shape.radius ** 2;
case 'square': return shape.side ** 2;
default: {
const _exhaustive: never = shape;
throw new Error(`Unhandled shape: ${_exhaustive}`);
}
}
}
For functions that need flexibility without abandoning type safety, use generic constraints instead of any. function process<T extends Record<string, unknown>>(data: T) accepts any object shape while remaining type-checkable, whereas function process(data: any) accepts anything and checks nothing.
Type Guards and Narrowing
TypeScript performs control flow analysis to narrow the type of a variable based on what checks precede any use of that variable. Understanding where narrowing occurs — and how to guide it — is one of the skills that separates mid-level from senior TypeScript developers.
Built-in narrowing covers typeof, instanceof, truthiness checks, and in operator checks. When built-in narrowing is insufficient, custom type guards let you encode the check in a reusable function with a typed return signature:
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
typeof (value as Record<string, unknown>).id === 'number' &&
'email' in value &&
typeof (value as Record<string, unknown>).email === 'string'
);
}
The value is User return type is the predicate signature — it tells TypeScript that after this function returns true, the variable should be narrowed to User within that branch.
Discriminated unions are the cleaner structural pattern. Adding a literal kind or type field to each member of a union gives TypeScript an unambiguous discriminant to switch on:
type ApiResult =
| { kind: 'success'; data: User[] }
| { kind: 'empty' }
| { kind: 'error'; message: string; statusCode: number };
function handleResult(result: ApiResult) {
switch (result.kind) {
case 'success': return renderUsers(result.data); // data is User[]
case 'empty': return renderEmptyState();
case 'error': return renderError(result.message); // message is string
}
}
This pattern appears throughout Redux state slices, React Query result objects, and custom API client responses in production Indian product company codebases.
Utility Types — The Standard Library
TypeScript ships a set of generic utility types in its standard library that should be known by every working TypeScript developer. Reaching for these before defining new interfaces prevents duplication and keeps types DRY:
Partial<T>— makes every property optional; the correct type for PATCH request bodies and state update payloads.Required<T>— makes every optional property required; useful for internal handlers that know an optional field is present.Readonly<T>— prevents property mutation; use for configuration objects and context values that should never be modified after creation.Pick<T, K>— creates a type containing only the specified subset of properties fromT; useful for projection types representing what an API endpoint actually returns.Omit<T, K>— creates a type excluding specified properties; the correct type for create payloads where the ID is server-generated.Record<K, V>— object type with keys of typeKand values of typeV; replaces{ [key: string]: V }with something more expressive.ReturnType<typeof fn>— extracts the return type of a function; avoids duplicating types when you want to type a variable holding the output of an API client method.Awaited<T>— unwraps Promise types recursively; use when you need the resolved value type of an async function without manually unwrappingPromise<T>.
The anti-pattern to avoid: writing a new interface UserSummary { id: number; name: string } when Pick<User, 'id' | 'name'> already expresses the same constraint and stays synchronized with the User source type automatically.
Template Literal and Mapped Types
TypeScript's type-level programming features allow constraints to be expressed at compile time that would otherwise require runtime validation. Two of the most practically valuable are template literal types and mapped types.
Template literal types construct string literal types from patterns. A common use is event name typing:
type DomEvent = 'click' | 'focus' | 'blur' | 'change';
type HandlerName = `on${Capitalize<DomEvent>}`;
// HandlerName = 'onClick' | 'onFocus' | 'onBlur' | 'onChange'
This means a function accepting a handler name parameter can be typed to accept only valid handler strings, with autocomplete support, without listing every combination manually.
Mapped types transform every property in an existing type using a mapped syntax:
type Nullable<T> = { [K in keyof T]: T[K] | null };
type DeepReadonly<T> = { readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K] };
type ReadonlyPartial<T> = { readonly [K in keyof T]?: T[K] };
Combining mapped types with conditional types produces the kind of expressive type transformations seen in mature libraries like Prisma, Zod, and tRPC — where the type system encodes the entire shape of database queries or API contracts, catching invalid operations before runtime.
Typing React Components
React and TypeScript have a long shared history, but several common patterns in React + TypeScript codebases reflect habits from older type definition packages rather than current best practice:
Avoid React.FC. The React.FC (formerly React.FunctionComponent) type implicitly added children to all component props regardless of whether the component accepted them — a source of confusion that led the TypeScript React community to largely abandon it after React 18. Prefer explicitly typed function declarations:
// Avoid:
const Button: React.FC<ButtonProps> = ({ label, onClick }) => { ... };
// Prefer:
function Button({ label, onClick }: ButtonProps) { ... }
Event handlers should use React's event types rather than any: React.MouseEvent<HTMLButtonElement>, React.ChangeEvent<HTMLInputElement>, React.FormEvent<HTMLFormElement>. These types give you correctly typed event.target and event.currentTarget properties.
useRef requires a generic type argument: useRef<HTMLDivElement>(null) types .current as HTMLDivElement | null, which is what DOM refs contain before mount. useRef<number>(0) for mutable value refs keeps the value type correct.
Custom hooks should have explicitly annotated return types. Without them, TypeScript infers the return type as a tuple of the specific values returned, which can produce confusing inference errors in components that consume the hook.
Context requires a typed initial value and a null-guard hook pattern. Creating a context with createContext<UserContextValue | null>(null) and consuming it through a hook that throws if the value is null prevents the common runtime error of accessing context outside its provider.
API Response Typing — The Real-World Pattern
A pervasive mistake in TypeScript codebases is casting API responses to expected types with the as keyword: const data = await response.json() as User. This cast is a promise to the compiler that you have not kept — if the API returns an unexpected shape, TypeScript trusts the cast and downstream code fails with confusing undefined or property access errors rather than a clear boundary error.
The production-grade pattern validates responses at the API boundary using a schema library, making the TypeScript type and the runtime validation a single source of truth:
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
role: z.enum(['admin', 'editor', 'viewer']),
createdAt: z.string().datetime(),
});
type User = z.infer<typeof UserSchema>;
async function fetchUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const raw = await response.json();
return UserSchema.parse(raw); // throws ZodError at boundary if shape is wrong
}
With this pattern, type User is inferred from the schema — it cannot drift from the validation logic. A mismatched API response fails immediately at the fetch boundary with a descriptive Zod error message pointing to the exact field that failed, rather than a silent undefined that surfaces five function calls later in a completely unrelated component.
Zod is the dominant choice in the Indian React/Next.js community, but Valibot (smaller bundle), Arktype (faster parse), and io-ts (older functional style) serve the same purpose. The pattern matters more than the library.
TypeScript in the Indian Developer Job Market
Indian product companies and well-funded startups have converged on a consistent proficiency model when evaluating TypeScript candidates:
- Junior level: Understands that types exist and writes basic interfaces; defaults to
anyunder pressure; uses type annotations on function parameters but rarely on return types. - Mid level: Reads and writes utility types; understands generics at a practical level; operates with strict mode enabled; avoids
anyin new code. - Senior level: Designs type systems that prevent entire bug categories; composes mapped and conditional types; can type complex third-party library APIs; uses the type system as a communication tool between modules and teams.
Interview patterns at Indian product companies for mid-to-senior TypeScript roles typically include: given a JavaScript function with observed runtime type errors, add TypeScript types to prevent them; explain the difference between interface and type with concrete examples; write a generic function with constraints that still enforces type relationships between arguments and return; explain what keyof and typeof extract and where each appears in production code.
The salary signal is measurable. Developers who demonstrate senior TypeScript proficiency — type system design, not just syntax knowledge — command ₹5–8 lakh premium over equivalent-experience JavaScript developers at Indian product companies, putting senior TypeScript roles at ₹18–35 lakh total compensation in Bangalore, Kochi, and Hyderabad. TypeScript is increasingly listed as a hard requirement, not a preference, in frontend and full-stack job postings at companies using React or Angular.
The investment to reach senior-level TypeScript fluency from a strong JavaScript base is roughly 3–6 months of deliberate practice — reading the official TypeScript Handbook, working through Total TypeScript by Matt Pocock, and migrating an existing JavaScript project to strict TypeScript. The return on that investment, in both code quality and compensation, makes it one of the highest-yield technical skills available to Indian developers in 2026.
Frequently Asked Questions
Should I use interface or type in TypeScript?
Use interface for object shapes that may be extended or implemented by classes, public API surfaces that consumers may augment via declaration merging, and object-oriented code with class hierarchies. Use type for union types (type Status = 'active' | 'inactive'), intersection types, mapped types, conditional types, and any type that cannot be expressed as an interface. Practical rule for React and Next.js developers: use type for component props and API response types (they often need union or intersection operations); use interface specifically when you need declaration merging or class implementation contracts.
How do I type a function that accepts different argument shapes?
Use TypeScript function overloads for distinct call signatures — write multiple function signatures above the implementation signature:
function format(value: string): string;
function format(value: number, decimals: number): string;
function format(value: string | number, decimals?: number): string {
// implementation handles both forms
if (typeof value === 'string') return value.trim();
return value.toFixed(decimals ?? 2);
}
Callers see only the overload signatures, not the implementation signature, so TypeScript correctly restricts valid argument combinations for each call form. For simpler cases, use a discriminated union parameter — function process(input: { kind: 'text'; value: string } | { kind: 'number'; value: number }): string — TypeScript narrows within the function body based on input.kind, giving complete type safety without a cast.
Is TypeScript worth learning for a JavaScript developer in India in 2026?
Yes — unambiguously. React, Vue, Angular, Next.js, NestJS, and tRPC all default to TypeScript in their starter templates. Senior JavaScript roles at Indian product companies increasingly list TypeScript as required, not preferred. The learning curve for basic TypeScript is 2–4 weeks for an experienced JavaScript developer. The productivity impact is immediate: fewer runtime errors in production, significantly better IDE autocomplete and refactoring support, and safe large-scale codebase changes. Developers who understand TypeScript's type system command ₹5–8 lakh premium over equivalent-experience JavaScript developers at Indian product companies, with ₹18–35 lakh total compensation for senior TypeScript roles in Bangalore, Kochi, and Hyderabad. Recommended learning path: TypeScript Handbook (official, free) → Total TypeScript by Matt Pocock → convert an existing personal JavaScript project to TypeScript with strict mode fully enabled.