Structure of a TypeScript interface
An interface defines the shape of an object: what properties it has and what types they are. Basic syntax: interface User { id: number; name: string; email: string; }. Each property has a name, colon, and its type. Primitive types are string, number, boolean, null, undefined.
Optional properties are marked with ?: avatar?: string means it may or may not exist. Readonly properties are immutable: readonly id: number can't be reassigned after creation. Arrays are typed with string[] or Array<string>, both valid.
For nested objects, you can define interfaces inline or reference other interfaces: address: { street: string; city: string } or address: Address where Address is another interface. The second option is preferable for reusability. Union types combine options: status: 'active' | 'inactive' | 'pending'.
Naming conventions and organization
Interface names use PascalCase: UserProfile, ApiResponse, ProductDetails. Avoid redundant prefixes like IUser (C# inheritance, unnecessary in TS). If you have name conflicts, use descriptive suffixes: UserEntity vs UserDto vs UserViewModel.
Organize interfaces by feature or layer. In a React project: components/Button/Button.types.ts for props, api/users/types.ts for responses. Don't throw everything in a global types.ts; that file grows out of control and nobody knows who's using what. Exception: globally shared types go in @types or shared/types.
For types from external APIs, consider autogenerating with tools like openapi-typescript or quicktype. Writing 200 lines of interface by hand for a Stripe response is guaranteed human error. If the API has OpenAPI/Swagger schema, generate it; if not, ask them to add it.
Utility types and composition
TypeScript includes utility types that transform existing interfaces. Partial<User> makes all props optional (useful for updates). Required<User> does the opposite. Pick<User, 'id' | 'name'> selects only those props. Omit<User, 'password'> excludes the password prop.
Record<string, number> is an object with string keys and number values, useful for dynamic mappings. Readonly<User> makes the entire interface immutable. NonNullable<T> removes null/undefined from a type. These utilities avoid duplication: you don't need a separate UserUpdate if you can use Partial<User>.
For composition, extend interfaces: interface Admin extends User { permissions: string[] }. Or use intersection: type AdminUser = User & { permissions: string[] }. The difference: extends is for interfaces (can be redeclared), & is for types (more flexible but not redeclarable).
Common mistakes and how to avoid them
Mistake #1: using any out of laziness. data: any destroys TypeScript's purpose. If you don't know the exact type, use unknown and do type narrowing with guards. Or define the interface partially and iterate: better to have 3 typed props than everything in any.
Mistake #2: overly generic interfaces. interface Data { value: any } adds nothing. Be specific: interface UserData { userId: string; loginCount: number }. The more concrete the type, the more errors the compiler catches.
Mistake #3: not typing API responses. Fetch returns any by default. Always type: const user = await fetch('/api/user').then(r => r.json()) as User or better, use a typed client like tRPC, GraphQL Codegen, or axios with interceptors that typecheck.
Mistake #4: duplicating definitions. If User in frontend and backend are identical, share the type (monorepo with shared package, or export from backend if it's TypeScript too). Keeping two definitions manually synced is a recipe for subtle bugs where a field changed type and frontend didn't find out.