TypeScript fluency is now baseline for senior frontend roles. Beyond the basics (interfaces, type aliases, basic generics), interviews probe whether you understand the type system as a small but capable functional language — capable of expressing complex constraints and catching bugs at compile time.
Generics deeper
Generic constraints with extends:
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
// ...
}
The constraint K extends keyof T means K must be a key of T. Misuse is caught at the call site.
Conditional types
The basic form:
type IsString<T> = T extends string ? true : false;
More useful: distribute over unions.
type ToArray<T> = T extends any ? T[] : never;
type R = ToArray<string | number>; // string[] | number[]
Mapped types
Transform every property:
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Partial<T> = { [K in keyof T]?: T[K] };
These are built-in but illustrate the pattern. Combine with conditional types for sophisticated transforms.
Template literal types
String types with template-like composition:
type EventName<T extends string> = `on${Capitalize<T>}`;
type R = EventName<'click'>; // 'onClick'
Used in libraries for inferring types from string keys (Tailwind classes, route paths, etc.).
Inferring with infer
Extract a type from another:
type ReturnType<T> = T extends (...args: any) => infer R ? R : never;
type R = ReturnType<() => string>; // string
Powerful for library types. Used in React for inferring component props, in Zod for inferring schemas.
Discriminated unions
One of the most-used patterns:
type Result<T> = { ok: true; value: T } | { ok: false; error: string };
Switching on the discriminator narrows the type:
if (result.ok) {
console.log(result.value); // type narrowed to T
} else {
console.log(result.error); // type narrowed to string
}
Branded types
Distinguish between types that share a structure:
type UserId = string & { __brand: 'UserId' };
type CompanyId = string & { __brand: 'CompanyId' };
function getUser(id: UserId) { /* ... */ }
const cid = "abc" as CompanyId;
getUser(cid); // type error — cannot pass CompanyId where UserId is expected
Useful for IDs, monetary amounts, units.
Const assertions
as const makes the type as narrow as possible:
const colors = ['red', 'green', 'blue'] as const;
type Color = typeof colors[number]; // 'red' | 'green' | 'blue'
Critical for deriving types from runtime values.
Types as tests
For library authors, type-level tests catch regressions:
type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false;
type _ = Expect<Equal<ReturnType<() => string>, string>>;
If the assertion fails, TypeScript errors at compile time.
Common pitfalls
anyas escape hatch — defeats the purposeascasts to bypass type errors — usually a bug masking- Excessive optionality (
?) when properties are actually required - Using
{}as “any object” — actually means non-null - Object index types like
[k: string]: any— loses type safety
Strict mode
Enable in tsconfig:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true
}
}
Strict mode + the additional flags catches more bugs. Mandatory for serious code.
The Zod pattern
Zod schemas double as runtime validators and types:
const Schema = z.object({ email: z.string().email(), age: z.number() });
type Data = z.infer<typeof Schema>;
One source of truth for runtime + compile-time. Used everywhere in 2026.
Frequently Asked Questions
Should I use TypeScript for small projects?
For projects above a single file, yes. The friction is minimal; the benefits compound.
How do I handle third-party libraries without types?
Look for @types/ definitions on DefinitelyTyped. If none exist, write a small declaration file or contribute to the library.
Are template literal types overkill?
For most app code: yes. For library code (especially type-safe APIs and DSLs): often essential.