The Problem
I have a class with several methods that make requests to the API. I would like to be able to display a loading indicator for each of these calls independently. I don't want to add new class field for each loading status indication property, and use one big object instead.
The question is: what type should I use for isLoading
in my class definition?
export class LibraryStore {
authors: Author[] = [];
books: Book[] = [];
isLoading: any /* ? */ = {};
fetchAuthors() {
this.isLoading.fetchAuthors = true;
try {
// ...
} catch (e) {
// ...
} finally {
this.isLoading.fetchAuthors = false;
}
}
fetchBooks() {
// ...
}
}
The Solution
The easiest and most obvious way to make it work is to use Record<string, boolean>
. But this type allows any string, not only methods' names.
Let's find out how to restrict this.
Step 1. KeyOfType
export type KeyOfType<Type, ValueType> = keyof {
[Key in keyof Type as Type[Key] extends ValueType ? Key : never]: any;
};
The above code shows a generic type that extracts keys from Type
, where Type[Key]
is of type ValueType
or a derivative thereof.
class SomeObject {
a = 1;
b = 'foo';
c = 'bar';
d = true;
}
const foobar: KeyOfType<SomeObject, string>; // 'b' | 'c'
And that's it. KeyOfType
is an answer to the question in title.
But let's take it a little bit further.
Step 2. ClassMethods
This is a simple type, which extracts fields that are functions.
export type ClassMethods<T> = KeyOfType<T, Function>;
Step 3. IsLoadingRecord
Take a look at the code below:
export type IsLoadingRecord<Type> = Partial<Record<ClassMethods<Omit<Type, 'isLoading'>>, boolean>>;
This is final type I use for isLoading
field.
export class LibraryStore {
authors: Author[] = [];
books: Book[] = [];
isLoading: IsLoadingRecord<LibraryStore> = {};
fetchAuthors() {
this.isLoading.fetchAuthors = true;
try {
// ...
} catch (e) {
// ...
} finally {
this.isLoading.fetchAuthors = false;
}
}
fetchBooks() {
// ...
}
}
Let me explain how it works.
ClassMethods<Omit<Type, 'isLoading'>>
ClassMethods
type returns all methods from Type
, except for isLoading
. You may ask, why?
If isLoading
is not omitted, using it inside Type
(like in example above) would end with TypeScript error:
TS2502: 'isLoading' is referenced directly or indirectly in its own type annotation.
Next, I use Record
with boolean
s as values
Record<ClassMethods<Omit<Type, 'isLoading'>>, boolean>
And finally, not all methods will use loading indicator, so I wrap it in Partial
Partial<Record<ClassMethods<Omit<Type, 'isLoading'>>, boolean>>
Now I can use properly typed isLoading
field with autocomplete.
Code editor shows proper autocomplete hints |
The End
Hope you enjoyed this quick journey. If you have any problem I could help you with, please leave a comment.
See you next time!
Top comments (0)