This TypeScript error occurs when you try to declare a class field (property with an initializer) inside an interface, which is not allowed. Interfaces can only define the shape of objects, not implement them. The fix involves converting the interface to a class or removing the field initializer.
The "Interface declaration cannot declare a class field" error (TS1166) appears when you attempt to include a class field with an initial value inside a TypeScript interface. Interfaces in TypeScript are purely structural contracts that define what properties and methods an object should have, but they cannot contain implementations or initial values. Interfaces serve as type definitions only - they describe the shape of data but don't actually create that data. When you try to give a property an initial value (like `name: string = "default"`), you're mixing interface declaration syntax with class implementation syntax. This error commonly occurs when developers: 1. Confuse interfaces with abstract classes 2. Try to add default values to interface properties 3. Copy class syntax into an interface declaration 4. Use interfaces where they need concrete implementations TypeScript enforces this separation to maintain clear boundaries between type definitions (interfaces) and runtime implementations (classes).
The simplest fix is to remove the assignment from the interface property:
// WRONG - interface with field initializer
interface User {
name: string = "Guest"; // Error: Interface declaration cannot declare a class field
age: number = 0; // Error
}
// CORRECT - interface without initializers
interface User {
name: string; // Type only, no value
age: number; // Type only, no value
}
// Usage
const user: User = {
name: "John", // Value assigned when creating object
age: 30,
};Interfaces define types, not values. The actual values must be provided when creating objects that implement the interface.
If you need default values, use a class instead:
// WRONG - trying to set defaults in interface
interface Config {
timeout: number = 5000;
retries: number = 3;
}
// CORRECT - use a class with default values
class Config {
timeout: number = 5000;
retries: number = 3;
constructor(overrides?: Partial<Config>) {
Object.assign(this, overrides);
}
}
// Usage
const config = new Config(); // timeout=5000, retries=3
const customConfig = new Config({ timeout: 10000 }); // timeout=10000, retries=3Classes support field initializers, constructors, and methods - everything you need for concrete implementations.
If you want optional properties with fallback values, use union types:
// WRONG
interface Settings {
theme: "light" | "dark" = "light";
notifications: boolean = true;
}
// CORRECT - make properties optional, handle defaults elsewhere
interface Settings {
theme?: "light" | "dark";
notifications?: boolean;
}
// Helper function to apply defaults
function createSettings(overrides?: Settings): Required<Settings> {
return {
theme: overrides?.theme ?? "light",
notifications: overrides?.notifications ?? true,
};
}
// Usage
const settings = createSettings(); // theme="light", notifications=true
const darkSettings = createSettings({ theme: "dark" }); // theme="dark", notifications=trueThis pattern separates the type definition (interface) from the default value logic (helper function).
For more complex scenarios, use type aliases with intersection types:
// Define base interface without defaults
interface UserBase {
name: string;
email: string;
}
// Define defaults as a separate type
type UserDefaults = {
role: "user" | "admin";
active: boolean;
};
// Create a type that combines them
type UserWithDefaults = UserBase & Partial<UserDefaults>;
// Helper to apply defaults
function createUser(base: UserBase, defaults?: Partial<UserDefaults>): UserBase & UserDefaults {
return {
...base,
role: defaults?.role ?? "user",
active: defaults?.active ?? true,
};
}
// Usage
const user = createUser(
{ name: "Alice", email: "[email protected]" },
{ role: "admin" }
);
// Result: { name: "Alice", email: "[email protected]", role: "admin", active: true }This approach gives you flexibility while keeping type definitions clean.
If you need to share implementation between classes, use abstract classes:
// WRONG - trying to share implementation via interface
interface Logger {
prefix: string = "[LOG]";
log(message: string): void;
}
// CORRECT - use abstract class
abstract class Logger {
protected prefix: string = "[LOG]";
abstract log(message: string): void;
protected formatMessage(message: string): string {
return `${this.prefix} ${new Date().toISOString()}: ${message}`;
}
}
// Concrete implementation
class ConsoleLogger extends Logger {
log(message: string): void {
console.log(this.formatMessage(message));
}
}
// Another implementation
class FileLogger extends Logger {
constructor() {
super();
this.prefix = "[FILE]";
}
log(message: string): void {
// Write to file using formatted message
const formatted = this.formatMessage(message);
// ... file writing logic
}
}Abstract classes can have field initializers and method implementations that subclasses inherit.
Combine interfaces with factory functions for maximum flexibility:
// Interface defines the shape
interface ApiClient {
baseUrl: string;
timeout: number;
get<T>(path: string): Promise<T>;
post<T>(path: string, data: unknown): Promise<T>;
}
// Factory function creates instances with defaults
function createApiClient(config?: Partial<Pick<ApiClient, 'baseUrl' | 'timeout'>>): ApiClient {
const baseUrl = config?.baseUrl ?? "https://api.example.com";
const timeout = config?.timeout ?? 30000;
return {
baseUrl,
timeout,
async get<T>(path: string): Promise<T> {
const response = await fetch(`${baseUrl}${path}`, { timeout });
return response.json();
},
async post<T>(path: string, data: unknown): Promise<T> {
const response = await fetch(`${baseUrl}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
timeout,
});
return response.json();
},
};
}
// Usage
const defaultClient = createApiClient(); // Uses all defaults
const customClient = createApiClient({
baseUrl: "https://staging.example.com",
timeout: 60000
});This pattern gives you both: clean interface definitions and configurable defaults.
### Interface vs Type Alias
Both interfaces and type aliases cannot have field initializers, but they have different capabilities:
// Interface - can be extended, merged
interface Point {
x: number;
y: number;
}
interface Point3D extends Point {
z: number;
}
// Type alias - more flexible with unions, intersections
type ID = string | number;
type Coordinates = { x: number; y: number };
type Point3D = Coordinates & { z: number };
// NEITHER can have field initializers
interface Bad {
x: number = 0; // Error
}
type AlsoBad = {
y: number = 0; // Error
};### Declaration Merging
Interfaces support declaration merging, which can be confusing when you see multiple declarations:
// First declaration
interface Config {
timeout: number;
}
// Second declaration (merged)
interface Config {
retries: number;
}
// Result: Config has both timeout and retries
const config: Config = {
timeout: 5000,
retries: 3,
};
// But still can't add initializers
interface Config {
timeout: number = 5000; // Error even in merged declaration
}### Readonly Properties
Interfaces can have readonly properties, but still no initializers:
interface Circle {
readonly radius: number;
readonly diameter: number; // Can be computed, but not initialized here
// WRONG
readonly pi: number = 3.14; // Error
// CORRECT - compute in implementation
}
class CircleImpl implements Circle {
readonly radius: number;
readonly diameter: number;
readonly pi: number = 3.14; // OK in class
constructor(radius: number) {
this.radius = radius;
this.diameter = radius * 2;
}
}### Generic Interfaces
Generic interfaces also cannot have field initializers:
interface Container<T> {
value: T;
// WRONG
defaultValue: T = null; // Error - can't initialize generic type
// CORRECT
defaultValue?: T; // Optional instead
}
// Factory pattern for generics
function createContainer<T>(value: T, defaultValue?: T): Container<T> {
return {
value,
defaultValue: defaultValue ?? null as any, // Type assertion if needed
};
}### Interface vs Abstract Class Decision Tree
Use this guide to choose between interfaces and abstract classes:
1. Use Interface when:
- Defining a contract for external code
- Need declaration merging
- Defining object shapes for libraries/APIs
- Lightweight type checking only
2. Use Abstract Class when:
- Need to share implementation between classes
- Want default method implementations
- Need field initializers
- Building a class hierarchy
- Need constructor logic
3. Use Regular Class when:
- Creating concrete instances
- Need full implementation
- Using with new keyword
- Runtime behavior needed
### Compiler Flags
TypeScript has compiler flags that affect interface behavior:
{
"compilerOptions": {
"strict": true, // Enables all strict checks
"noImplicitAny": true,
"strictNullChecks": true,
"strictPropertyInitialization": true // Requires explicit initialization in classes
}
}The strictPropertyInitialization flag is particularly relevant - it requires class properties to be initialized in the constructor, which might lead developers to incorrectly try initializing interface properties instead.
### Migration from JavaScript
JavaScript developers transitioning to TypeScript often make this error when converting plain object patterns:
// JavaScript pattern
const defaults = {
timeout: 5000,
retries: 3,
};
// Incorrect TypeScript translation
interface Defaults {
timeout: number = 5000; // Error
retries: number = 3;
}
// Correct TypeScript translation
interface Defaults {
timeout: number;
retries: number;
}
const defaults: Defaults = {
timeout: 5000,
retries: 3,
};Remember: Interfaces define types, objects hold values.
Function expression requires a return type
Function expression requires a return type
Value of type 'string | undefined' is not iterable
How to fix "Value is not iterable" in TypeScript
Type 'undefined' is not assignable to type 'string'
How to fix "Type undefined is not assignable to type string" in TypeScript
Type narrowing from typeof check produces 'never'
How to fix "Type narrowing produces never" in TypeScript
Type parameter 'T' has conflicting constraints
How to fix "Type parameter has conflicting constraints" in TypeScript