Say I have an Angular service TodosApiService
with the following shape:
@Injectable()
class TodosApiService {
constructor(private http: HttpClient) {}
getTodo(id: number): Observable<Todo> {}
getTodos(): Observable<Todo[]> {}
createTodo(todo: Todo): Observable<number> {}
updateTodo(todo: Todo): Observable<void> {}
deleteTodo(id: number): Observable<void> {}
private checkCache(id: number): Todo | null
}
Now say I want to code a class MockTodosApiService
that can be used in place of TodosApiService
and which returns mock data instead of making actual network requests. This might be useful for situations like testing or developing components in isolation in a system like Storybook.
How can I ensure that my mock class provides the same interface as TodoApisService
? I.e. that I am implementing all the public methods from the service, and that I am returning the correct types? And that if somebody comes along and adds a method bulkUpdateTodos()
to TodosApiService
that they get a compile error unless they also add it to MockTodosApiService
?
One way would be to create a TypeScript interface
that both TodosApiService
and MockTodosApiService
implement:
interface TodosApi {
getTodo(id: number): Observable<Todo> {}
getTodos(): Observable<Todo[]> {}
createTodo(todo: Todo): Observable<number> {}
updateTodo(todo: Todo): Observable<void> {}
deleteTodo(id: number): Observable<void> {}
}
class TodosApiService implements TodosApi { }
class MockTodosApiService implements TodosApi { }
In languages like C# or Java that would be my only real option, but it's kind of a pain. I now have three places to make changes keep in sync with each other.
However, the dynamic nature of JS and TypeScript's expressiveness provide a different option. I can instead use mapped types to tell the compiler that I want my mock class to provide the same public interface as the "real" service. It's as simple as this:
class MockTodosApiService implements Pick<TodosApiService, keyof TodosApiService> { }
TypeScript will now complain if I implement MockTodosApiService
in a way that is incompatible with the public interface of TodosApiService
. However, it will not complain about my not replicating the constructor, or the private checkCache()
method and http
field.
This works because keyof
returns the names of every public field and method of a type. We then use the Pick<>
mapped type, which returns a subset of a type's properties. The result is a type that contains only those properties from TodosApiService
that are returned by keyof TodosApiService
. And by saying that MockTodosService
implements
that type, I get compile-time checking that I'm doing so correctly.
Top comments (0)