Reputation: 7965
I'm struggling with how to strongly type some functionality with TypeScript.
Essentially I have a function that accepts a key/value map of DataProviders and returns a key/value map of the data returned from each. Here's a simplified version of the problem:
interface DataProvider<TData> {
getData(): TData;
}
interface DataProviders {
[name: string]: DataProvider<any>;
}
function getDataFromProviders<TDataProviders extends DataProviders>(
providers: TDataProviders): any {
const result = {};
for (const name of Object.getOwnPropertyNames(providers)) {
result[name] = providers[name].getData();
}
return result;
}
Currently getDataFromProviders
has a return type of any
but I want it so that if called like so...
const values = getDataFromProviders({
ten: { getData: () => 10 },
greet: { getData: () => 'hi' }
});
...then values
will be implicitly strongly typed as:
{
ten: number;
greet: string;
}
I imagine this would involve returning a generic type with a generic parameter of TDataProviders
but I can't quite work it out.
This is the best I can come up with but doesn't compile...
type DataFromDataProvider<TDataProvider extends DataProvider<TData>> = TData;
type DataFromDataProviders<TDataProviders extends DataProviders> = {
[K in keyof TDataProviders]: DataFromDataProvider<TDataProviders[K]>;
}
I'm struggling coming up with a DataFromDataProvider
type that compiles without me passing in TData
explicitly as a second parameter, which I don't think I can do.
Any help would be greatly appreciated.
Upvotes: 4
Views: 3726
Reputation: 51629
Imagine that you have a type that maps provider name to the data type returned by the provider. Something like this:
interface TValues {
ten: number;
greet: string;
}
Note that you don't actually have to define this type, just imagine it exists, and use it as generic parameter, named TValues
, everywhere:
interface DataProvider<TData> {
getData(): TData;
}
type DataProviders<TValues> =
{[name in keyof TValues]: DataProvider<TValues[name]>};
function getDataFromProviders<TValues>(
providers: DataProviders<TValues>): TValues {
const result = {};
for (const name of Object.getOwnPropertyNames(providers)) {
result[name] = providers[name].getData();
}
return result as TValues;
}
const values = getDataFromProviders({
ten: { getData: () => 10 },
greet: { getData: () => 'hi' }
});
magically (in fact, using inference from mapped types, as @Aris2World pointed out), typescript is able to infer correct types:
let n: number = values.ten;
let s: string = values.greet;
update: as pointed out by the author of the question, getDataFromProviders
in the code above does not really check that each property of the object it receives conforms to DataProvider
interface.
For example, if getData
is misspelled, there is no error, just empty object type is inferred as return type of getDataFromProviders
(so you will still get an error when you try to access the result, however).
const values = getDataFromProviders({ ten: { getDatam: () => 10 } });
//no error, "const values: {}" is inferred for values
There is a way to make typescript to detect this error earlier, at the expense of additional complexity in DataProviders
type definition:
type DataProviders<TValues> =
{[name in keyof TValues]: DataProvider<TValues[name]>}
& { [name: string]: DataProvider<{}> };
The intersection with indexable type adds a requirement that every property of DataProviders
must be compatible with DataProvider<{}>
. It uses empty object type {}
as generic argument for DataProvider
because DataProvider
has the nice property that for any data type T
, DataProvider<T>
is compatible with DataProvider<{}>
- T
is the return type of getData()
, and any type is compatible with empty object type {}
.
Upvotes: 8