Both describe object shapes and are interchangeable for most models and props. An interface describes only objects and can be reopened to add fields (declaration merging); a type alias can name any type. A common default: interface for objects, type for the rest.
**Only type**: unions, tuples, function types, mapped and conditional types, the result of keyof or typeof.
**Only interface**: declaration merging. Two interface Window blocks merge into one, which is how .d.ts files extend global and library types; a second type with the same name is a "Duplicate identifier" error.
Extending: interface Admin extends User reports an incompatible property at the declaration; User & {...} silently turns it into never.
Checker speed: TypeScript's performance guide prefers extends over &, because relationships between interfaces are cached and intersections are rechecked.
ReactMiddle
TypeScript Type System
type vs interface, any, unknown and never, narrowing and type guards, structural typing, enums and literal unions, generics and constraints, keyof and typeof, mapped and conditional types, infer, declaration files
Questions closed
0 / 18
- 0Review
- 8Asked often
Questions
any switches type checking off: anything can be assigned to it and you can do anything with it. unknown also accepts any value, but you must narrow it before use, which makes it the safe type for unchecked data. never is the empty type: no value has it.
**any spreads**: loose.nmae.toUpperCase() compiles, and anything read from an any is any too, so one of them quietly disables checks downstream. JSON.parse and res.json() return any.
**unknown forces a check** (typeof, in, a type guard, a schema) before you read or call it. Under strict it is the type of e in catch (e); use it for parsed JSON and instead of any in your own APIs.
**never**: the return type of a function that always throws, the type left after every case is handled (the exhaustiveness check), and impossible types such as string & number.
Top and bottom: every type is assignable to unknown, never is assignable to every type, and any goes both ways.
They disappear: the compiler erases every annotation, interface and type alias, and the JavaScript that runs has no trace of them. instanceof is a runtime check against a constructor function, and an interface has no runtime value, so TypeScript reports "'User' only refers to a type".
Erasure: tsc, esbuild, SWC and Babel strip the types and emit plain JavaScript; Node 24 runs .ts files directly the same way. Types cost nothing and check nothing at runtime.
**Shape checks instead of instanceof**: a literal kind field, the in operator ('name' in value), or a type guard isUser(value): value is User.
What survives: classes (so instanceof Account works), plus enum, a namespace with values and parameter properties, which emit code; the erasableSyntaxOnly flag (TypeScript 5.8) forbids them.
Generics are erased too: new T() is impossible; pass a constructor or a factory instead.
Narrowing is TypeScript following your runtime checks through the code: after if (typeof x === 'string'), x is a string inside the branch and the rest of the union after an early return. It narrows on typeof, instanceof, in, equality, truthiness, Array.isArray and type guards.
**typeof** handles primitives; typeof x === 'object' still leaves null, because typeof null is 'object'.
Truthiness: if (name) removes null and undefined, but also '' and 0; compare with != null when those are valid values.
**Equality and switch**: x === null, x == null (both null and undefined), or a literal field of a discriminated union.
**in and instanceof**: 'href' in target keeps the members that have href; error instanceof TypeError narrows to the class.
Where it is lost: in a callback, a narrowed let that is reassigned later goes back to its declared type; copy it into a const first.
A union A | B is a value of either type, so until you narrow you can use only what both have. An intersection A & B is both at once and has every property of each. A property declared in both gets the intersection of its types, so string & number makes it never.
Think in sets of values: | admits more values and guarantees fewer properties; & admits fewer values and guarantees more. string & number has no values at all, hence never.
The conflict: { id: string } & { id: number } still compiles, but no object satisfies it, and the error appears only where you build one. If the conflicting property is a literal discriminant (kind: 'circle' against kind: 'square'), the whole intersection reduces to never.
Unions of functions: ((v: string) => void) | ((v: number) => void) can be called only with an argument both accept, so the parameter becomes string & number, which is never.
Structural typing means a value fits a type if it has the required properties, whatever else it carries, so p passes. An object literal written right at the call is "fresh", and TypeScript adds an excess property check to it, because an unknown key there is almost always a typo.
Structural, not nominal: types with the same shape are compatible even if declared separately, unlike in Java, C#, Swift or Kotlin.
Freshness: the check applies only to a literal passed or assigned straight to a typed target; stored in a variable first, it is an ordinary value and extra keys are allowed.
Weak types: a type with only optional properties, such as { color?: string }, also rejects a variable that shares none of them ("has no properties in common"), so a stored { colour: 'red' } fails too.
When the extra key is intended: widen the target type or add an index signature; do not silence the error with as Point.
A type guard returns value is T: when it returns true, TypeScript narrows the argument to T in that branch. An assertion function, asserts value is T, throws when the check fails, so the code after the call is narrowed. The risk: the compiler trusts the signature and never checks the body.
A lying guard: if isUser checks only 'id' in value but claims value is User, callers trust a name that may not exist, and the crash happens far away. Check every field you claim, or derive the guard from a schema (Zod's safeParse).
**The false branch narrows too**: after if (!isUser(x)), User is removed from the type of x, so a guard that wrongly returns false mis-types the else branch.
Inferred guards: since TypeScript 5.5, filter((id) => id !== undefined) returns string[] without a hand-written predicate.
Assertion functions suit invariants at the top of a function. Call them through a name with an explicit type, or TypeScript reports "Assertions require every name in the call target to be declared with an explicit type annotation".
An enum is one of the few TypeScript features that emits runtime code, and it behaves unlike the rest of the type system: numeric enums map values back to names, string enums reject plain literals. Most teams use a string literal union, plus an as const object for runtime values.
Not erasable: an enum compiles to an object, so tools that only strip types cannot run it: Node fails with "TypeScript enum is not supported in strip-only mode", and the erasableSyntaxOnly flag bans enums.
Numeric quirks: the reverse mapping makes Object.keys(Direction) return ['0', '1', 'Up', 'Down'], and any number variable is assignable to the enum.
String enums are nominal: setStatus('active') fails although the value is exactly 'active'; callers must import the enum, and JSON data needs a cast.
**const enum is inlined and leaves no object, but inlining across files needs the whole program, which per-file transpilers (esbuild, SWC, Babel) do not have.
The replacement**: an as const object with type Role = (typeof Role)[keyof typeof Role] gives named constants and a literal union that a plain string from JSON also fits.
Config is { api: string; retries: number; features: string[] }, ConfigKey is 'api' | 'retries' | 'features', Retries is number, Feature is just string, Role is 'admin' | 'editor', and Dict is string | number.
**typeof in a type position** takes the type of a value; in an expression it is still JavaScript's runtime operator that returns a string such as 'object'. For an object literal TypeScript widens the literals, so 3 becomes number.
**keyof T** is the union of T's keys as literal types.
**T['retries']** reads one property's type; a union of keys reads several (Config['api' | 'retries'] is string | number), and T[number] reads an array's element type.
**Why Feature is string**: features was widened to string[] when config was declared. as const keeps roles as readonly ['admin', 'editor'], so [number] yields the literals; this is the usual way to derive a union from a list of values.
**Why Dict includes number**: JavaScript converts numeric keys to strings, so a string index signature also accepts obj[0], and keyof reports both.
Give the function two type parameters, T for the object and K extends keyof T for the key, and return T[K]. The constraint says K must be one of T's keys, so a wrong key is a compile error, and the result has the type of exactly that property.
Inference does the work: getProperty(user, 'age') infers T from user and K as the literal 'age', so the result is number; nobody writes type arguments.
Constraints in general: an unconstrained T is treated like unknown inside the function; T extends { id: string } lets the body read item.id while callers keep their full type.
**const type parameters** (TypeScript 5.0) infer literal types as if the caller wrote as const: paths(['/', '/settings']) stays readonly ['/', '/settings'].
A type parameter should appear twice: one that links nothing, such as function len<T extends string>(s: T): number, is just (s: string) written the hard way.
Overloads are several call signatures written above one implementation: callers see only the overloads, and the implementation must handle all of them. They pay off when the return type depends on which arguments are passed; when it does not, one signature with a union parameter is simpler.
The implementation signature is hidden: load(arg: string | string[]) cannot be called directly, and it must be compatible with every overload above it.
Resolution goes top to bottom: TypeScript picks the first overload that matches, so put the specific ones first.
A union argument matches no overload: with overloads for string and string[], a caller holding string | string[] gets "No overload matches this call". That is why the handbook says to prefer union parameters when you can.
The body is not checked against each overload: inside it the parameter is the wide union, and a branch that returns a User for an array of ids still compiles.
A .d.ts file holds only type declarations and no code: it describes JavaScript that exists elsewhere. For an untyped package, install @types/<name> or write declare module '<name>'; to add a global such as window.analytics, augment the Window interface inside declare global.
Where types come from: TypeScript packages ship their own .d.ts (types in package.json); others have community types on DefinitelyTyped (@types/lodash); browser APIs live in lib.dom.d.ts.
Quick shim: declare module 'legacy-chart'; alone makes every import from it any; declaring the functions you use keeps the checks.
Module or script: a .d.ts with a top-level import or export (even export {}) is a module whose declarations stay local, so global additions go inside declare global { ... }. In a file with no imports or exports, a top-level interface Window augments the global one directly.
The file must be in the program (covered by include in tsconfig.json), or its declarations are ignored.
A mapped type builds an object type by looping over a union of keys: { [K in Keys]: T[K] }. Pick loops over the chosen keys, Omit over all keys with the unwanted ones remapped to never, and DeepPartial makes every property optional and recurses into nested objects.
Modifiers: add readonly or ?, or remove them with -readonly and -?; that is how Required and a Mutable helper work.
Homomorphic mapping keeps modifiers: mapping over keyof T (or over a K extends keyof T) preserves each property's ? and readonly, which is why MyPick keeps id read-only.
**Key remapping with as** (TypeScript 4.1): a key mapped to never is dropped, and as get${Capitalize<K & string>} renames keys.
**The built-in Omit is loose**: it accepts any key (K extends keyof any), so Omit<User, 'idd'> silently removes nothing; the version here constrains K extends keyof T and catches the typo.
**DeepPartial edge cases**: functions and dates are objects too; without the function branch onSave would become {}, and a Date still turns into optional methods, so real versions special-case such types.
A conditional type T extends U ? X : Y picks a type by whether T is assignable to U, a ternary for types. Inside extends, infer R declares a type variable that TypeScript fills in by matching, so T extends (...args: any[]) => infer R ? R : never extracts the return type.
Unwrapping: T extends Promise<infer V> ? UnwrapPromise<V> : T recurses through Promise<Promise<number>> down to number; the built-in Awaited does the same and also handles any thenable.
**Other places for infer**: an array's element ((infer E)[]), the first tuple element ([infer H, ...unknown[]]), a function's parameters, or part of a string in a template literal type.
The standard library is built this way: ReturnType, Parameters, InstanceType, Awaited, Exclude and Extract are all conditional types.
Keep them in utility types: nested conditionals slow the checker and produce unreadable errors; in app code a named interface stays clearer.
Template literal types build string literal types with backtick syntax: on${Capitalize<E>} turns 'click' into 'onClick', and a union expands into every combination. With key remapping in a mapped type, they generate a whole object of handler props from a map of events.
Intrinsic helpers: Capitalize, Uncapitalize, Uppercase and Lowercase transform literal types.
Unions multiply: ${'sm' | 'lg'}-${'red' | 'blue'} is four literals; at around 100,000 combinations TypeScript gives up with "Expression produces a union type that is too complex to represent".
Patterns, not only names: ${number}px accepts '12px' and rejects '12'; --${string} types CSS custom properties; /users/${string} types a route.
**infer works inside**: H extends on${infer E} ? Uncapitalize<E> : never goes back from 'onSelect' to 'select'; typed routers extract :id parameters from a path string the same way.
**K & string**: Capitalize needs a string, while keyof T can also contain number and symbol keys, so intersect with string first.
A is string[] | number[], B is (string | number)[], C is never and D is never[]. A conditional type over a bare type parameter is distributive: it runs once per union member and joins the results. [T] is not a bare parameter, so the union is checked as a whole.
Why it distributes
TypeScript splits a union only when it is substituted for a bare type parameter on the left of extends. Written out directly, string | number extends string ? 'y' : 'n' gives 'n'; through IsString<T> = T extends string ? 'y' : 'n', IsString<string | number> gives 'y' | 'n'.
never is the empty union, so distributing over it runs zero times and returns never: that is C. It also breaks the naive IsNever<T> = T extends never ? true : false, which returns never instead of true.
boolean is the union true | false, so ToArray<boolean> is false[] | true[], a surprise when a flag type passes through a distributive helper.
Which behaviour to want
Distribute for per-member work: filtering a union (Exclude<T, U> is T extends U ? never : T) or transforming each member of a discriminated union separately.
**Wrap in [T]** for questions about the type as a whole: IsNever, "is this exactly boolean", or when a union must stay one array type, as in B.
Intersect the base type with a marker property no real value has: type UserId = string & { readonly [brand]: 'UserId' }. A plain string or an OrderId lacks that marker, so passing one where a UserId is expected fails, and branded values come only from one function that validates and casts.
Why it is needed
TypeScript is structural, so type UserId = string is only another name: getOrders(orderId) compiles, and swapped ids turn into a production bug such as loading or deleting the wrong record.
How it works
The brand exists only in the type. At runtime the value is a plain string, so it costs nothing and serializes as is.
A unique symbol key (declare const brand: unique symbol) cannot clash with a real property, unlike a __brand string key.
A branded value is still a string, so userId.toUpperCase() works; only the reverse direction, from string to UserId, is blocked.
The cost
Every boundary (API responses, route params, form input) needs a conversion. A schema can do it in one step: Zod's z.string().brand<'UserId'>() validates and brands.
The same trick models units and validated values: Cents, Milliseconds, Email, NonEmptyString.
Covariance keeps the direction of subtyping: a Dog[] works as an Animal[]. Contravariance reverses it: a handler of any Animal can replace a handler of Dog. Under strictFunctionTypes function parameters are contravariant; method parameters stay bivariant to keep arrays covariant.
The rules
Return types are covariant: a function returning Dog fits where one returning Animal is expected.
Parameters are contravariant: where (a: Animal) => void is expected, (d: Dog) => void is unsafe, because the caller may pass a Cat. strictFunctionTypes (part of strict) rejects it for function-typed properties, variables and callbacks.
Methods are bivariant: a member written in method syntax, handle(a: Animal): void, accepts either direction. Checking methods strictly would make Dog[] not assignable to Animal[], because Array<T> methods such as push(item: T) take T as a parameter.
The consequences
Both holes are real: m.handle(cat) compiles and throws "dog.bark is not a function", and animals.push(cat) puts a cat into dogs.
In your own types, declare callbacks as properties, onSelect: (item: T) => void, to get the strict check; typescript-eslint's method-signature-style rule enforces it.
in and out on type parameters (TypeScript 4.7) state the variance explicitly; the compiler checks them against the structure and can skip a structural comparison in large generic types.