← Back to snippets
OptionalMethods<T> Type
A TypeScript utility type that makes all method properties optional while keeping data properties required
typescript
intermediate
A TypeScript utility type that makes method properties (functions) optional while preserving all other properties as required. Useful for implementing interfaces, creating mocks, and building class factories.
Snippet
type OptionalMethods<T> = {
[K in keyof T as T[K] extends (...args: any[]) => any ? K : never]?: T[K];
} & {
[K in keyof T as T[K] extends (...args: any[]) => any ? never : K]: T[K];
};
Usage Example
// Original interface with required methods and properties
interface User {
id: number;
name: string;
getFullName(): string;
updateProfile(data: any): Promise<void>;
isActive: boolean;
}
// Methods are optional, data properties remain required
type UserWithOptionalMethods = OptionalMethods<User>;
// ✅ Valid - can omit method properties
const user: UserWithOptionalMethods = {
id: 123,
name: "Jane Doe",
isActive: true
// getFullName and updateProfile are optional
};
// ❌ Error - cannot omit non-method properties
const invalidUser: UserWithOptionalMethods = {
id: 456,
// Error: Property 'name' is missing
isActive: false
};
How It Works
type OptionalMethods<T> = {
// First mapped type: selects only method properties and makes them optional
[K in keyof T as T[K] extends (...args: any[]) => any ? K : never]?: T[K];
} & {
// Second mapped type: selects only non-method properties and keeps them required
[K in keyof T as T[K] extends (...args: any[]) => any ? never : K]: T[K];
};
The type uses:
- Key remapping with
as
to filter properties - Conditional types to check if each property is a function
- Intersection to combine both filtered property sets
Real World Example
// Component interface with lifecycle methods and required properties
interface Component {
// Required data properties
id: string;
isVisible: boolean;
children: any[];
// Methods that might be implemented
render(): HTMLElement;
componentDidMount(): void;
componentWillUnmount(): void;
handleEvent(event: Event): void;
}
// Factory function that creates components with optional method implementations
function createComponent(config: OptionalMethods<Component>): Component {
// Default implementations for methods
const defaults = {
render: () => document.createElement('div'),
componentDidMount: () => console.log('Mounted'),
componentWillUnmount: () => console.log('Unmounting'),
handleEvent: (event: Event) => console.log('Event handled', event)
};
// Merge provided config with defaults
return { ...defaults, ...config };
}
// Usage - only implement the methods you need
const myComponent = createComponent({
id: 'my-component',
isVisible: true,
children: [],
// Only override the render method
render: () => {
const el = document.createElement('button');
el.textContent = 'Click me';
return el;
}
});
Testing Scenario
// Service interface with multiple methods
interface AuthService {
userId: string;
isLoggedIn: boolean;
login(username: string, password: string): Promise<boolean>;
logout(): Promise<void>;
refreshToken(): Promise<string>;
validateSession(): boolean;
}
// Create a mock with stub implementations only where needed
function createMockAuthService(
overrides: OptionalMethods<AuthService>
): AuthService {
// Default no-op implementations
return {
userId: overrides.userId || 'test-user',
isLoggedIn: overrides.isLoggedIn ?? false,
login: overrides.login || (async () => true),
logout: overrides.logout || (async () => {}),
refreshToken: overrides.refreshToken || (async () => 'mock-token'),
validateSession: overrides.validateSession || (() => true)
};
}
// Test with specific behavior for login method only
const mockAuth = createMockAuthService({
userId: 'test-123',
isLoggedIn: true,
// Only implement the methods relevant to the test
login: async (username, password) => {
expect(username).toBe('testuser');
expect(password).toBe('password');
return true;
}
});
Type Safety Considerations
The utility preserves the method signatures, ensuring type safety when methods are used:
interface Calculator {
value: number;
add(n: number): number;
subtract(n: number): number;
multiply(n: number): number;
}
const calc: OptionalMethods<Calculator> = {
value: 0,
// Type checking still works on method parameters
add: (n: number) => n + 10, // Correct
// Error: Type 'string' is not assignable to type 'number'
subtract: (n: number) => `${n}`
};
Comparison with Partial<T>
Unlike Partial<T>
which makes all properties optional, OptionalMethods<T>
is more selective:
interface Example {
id: number;
name: string;
calculate(): number;
}
// All properties are optional
type WithPartial = Partial<Example>;
// ✅ Valid - can omit any property
const a: WithPartial = {};
// Only methods are optional
type WithOptionalMethods = OptionalMethods<Example>;
// ❌ Error - data properties are still required
const b: WithOptionalMethods = {};
// ✅ Valid - data properties provided
const c: WithOptionalMethods = { id: 1, name: 'test' };