TypeScript General

I am struggling with how hard it is to typeset some functionality with TypeScript.

Basically, I have a function that takes 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 any

, but I want it to be called like this ...

const values = getDataFromProviders({
    ten: { getData: () => 10 },
    greet: { getData: () => 'hi' }
});

      

... values

would then be implicitly strongly typed as:

{
    ten: number;
    greet: string;
}

      

I guess it has something to do with returning a generic type with a shared parameter TDataProviders

, but I can't seem to completely solve it.

This is the best I can think of, but I don't compile ...

type DataFromDataProvider<TDataProvider extends DataProvider<TData>> = TData;

type DataFromDataProviders<TDataProviders extends DataProviders> = {
    [K in keyof TDataProviders]: DataFromDataProvider<TDataProviders[K]>;
}

      

I'm trying to come up with a type DataFromDataProvider

that compiles without me passing in TData

explicitly as the second parameter, which I don't think I can do.

Any help would be greatly appreciated.

+3


source to share


1 answer


Imagine you have a type that maps the provider name to the data type returned by the provider. Something like that:

interface TValues {
    ten: number;
    greet: string;
}

      

Note that you don't need to define this type , just pretend it exists and use it as a generic named parameter 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 the correct types

let n: number = values.ten;
let s: string = values.greet;

      



update : As the author of the question pointed out, the getDataFromProviders

code above doesn't actually check that every property of the retrieved object matches an interface DataProvider

.

For example, if it getData

has a spelling error, there is no error, only an empty object type is inferred as the return type getDataFromProviders

(so you will still get an error when you try to access the result).

const values = getDataFromProviders({ ten: { getDatam: () => 10 } });

//no error, "const values: {}" is inferred for values

      

There is a way to make typescript detect this error earlier, at the cost of additional complexity in the type definition DataProviders

:

type DataProviders<TValues> = 
    {[name in keyof TValues]: DataProvider<TValues[name]>}
   & { [name: string]: DataProvider<{}> };

      

Intersection with an index type adds a requirement that each property DataProviders

must be compatible with DataProvider<{}>

. It uses an empty object type {}

as a generic argument for DataProvider

, because it DataProvider

has the nice property that for any type of data T

, DataProvider<T>

compatible with DataProvider<{}>

- T

is the return type getData()

, and any type compatible with the type of an empty object {}

.

+6


source







All Articles