Skip to content Skip to sidebar Skip to footer

Typescript Map Of Arbitrary Generics

I'm trying to define two types, which should look something like: export type IQuery = { request: string; params: (props: P, upsteam?: U) => object; key: (pro

Solution 1:

The simplest thing you can do is this:

export type QueryMap = {
  [k: string]: IQuery<any, any>
};

It's not completely type-safe, but it is not too far off what you're trying to represent. If you don't want to lose type information for a value of type QueryMap, allow the compiler to infer a narrower type and use a generic helper function to ensure it is a valid QueryMap, like this:

const asQueryMap = <T extends QueryMap>(t: T) => t;

const queryMap = asQueryMap({
  foo: {
    request: "a",
    params(p: string, u?: number) { return {} },
    key(p: string, u?: number) { return "hey" },
    forceRequest: true
  }
});

The value queryMap.foo.params is still known to be a method that accepts a string and an optional number, even though the type QueryMap['foo']['params'] isn't.

If you specify something not assignable to a QueryMap you will get an error:

const bad = asQueryMap({
  foo: {
    request: "a",
    params(p: string, u?: number) { return {} },
    key(p: string, u?: number) { return "hey" },
    forceRequest: true
  },
  bar: {
    request: 123,
    params(p: number, u?: string) {return {}},
    key(p: number, u?: string) {return "nope"},
    forceRequest: false
  }
}); // error! bar.request is a number

The not-completely type-safe problem is shown here:

const notExactlySafe = asQueryMap({
  baz: {
    request: "a",
    params(p: number, u?: string) { return {} },
    key(p: string, u?: number) { return "hey" },
    forceRequest: true
  }
});

This is accepted, even though there's no consistent reasonable values of P and U that works here (which is what happens when you use any). If you need to lock this down more, you can try to have TypeScript infer sets of P and U values from the value or warn you if it cannot, but it's not staightforward.

For completeness, here's how I'd do it... use conditional types to infer P and U for each element of your QueryMap by inspecting the params method, and then verify that the key method matches it.

const asSaferQueryMap = <T extends QueryMap>(
  t: T & { [K in keyof T]:
    T[K]['params'] extends (p: infer P, u?: infer U) => any ? (
      T[K] extends IQuery<P, U> ? T[K] : IQuery<P, U>
    ) : never
  }
): T => t;

Now the following will still work:

const queryMap = asSaferQueryMap({
  foo: {
    request: "a",
    params(p: string, u?: number) { return {} },
    key(p: string, u?: number) { return "hey" },
    forceRequest: true
  }
});

while this will now be an error:

const notExactlySafe = asSaferQueryMap({
  baz: {
    request: "a",
    params(p: number, u?: string) { return {} },
    key(p: string, u?: number) { return "hey" },
    forceRequest: true
  }
}); // error, string is not assignable to number

This increases your type safety marginally at the expense of a fairly complicated bit of type juggling in the type of asSaferQueryMap(), so I don't know that it's worth it. IQuery<any, any> is probably good enough for most purposes.


Okay, hope that helps; good luck!


Solution 2:

You could use IQuery<any, any>.

I'm not sure what you're hoping for in the second part of the question. TypeScript doesn't give you runtime type information. If you just want to have type variables to refer to as you manipulate a single IQuery, you can pass an IQuery<any, any> to a function myFunction<P, U>(iquery: IQuery<P, U>) { ... }.


Solution 3:

The Solution

I removed from your types unrelevant information just for clarity. The solution boils-down to basically add 3 lines of code.


type Check<T> = QueryMap<T extends QueryMap<infer U> ? U : never>

export type IQuery<P, U, TQueryMap extends Check<TQueryMap>> = {
    prop1: (param1: P, param2?: U) => number;
    prop2: (param1: P, param2?: U) => string;
    prop3?: TQueryMap
}

export type QueryMap<T> = {
  [K in keyof T]: T[K]
};

// type constructors
const asQueryMap = <T>(x: QueryMap<T>) => x
const asQuery = <P, U, V extends QueryMap<any>>(x: IQuery<P, U, V>) => x

Considerations

All types are correctly infered by the compiler.

Important: If (and only if) you use the type constructors (see above) to construct yours structures you can consider yourself totally statically type-safe.

Bellow are the test cases:

Test of no compile errors


// Ok -- No compile-time error and correctly infered !

const queryMap = asQueryMap({
    a: asQuery({
        prop1: (param1: string, param2?: number) => 10,
        prop2: (param1: string, param2?: number) => "hello",
    }),

    b: asQuery({
        prop1: (param1: string, param2?: string) => 10,
        prop2: (param1: string, param2?: string) => "hello",
    }),

    c: asQuery({
        prop1: (param1: Array<number>, param2?: number) => 10,
        prop2: (param1: Array<number>, param2?: number) => "hello",
    })
})


const query = asQuery({
    prop1: (param1: string, param2?: number) => 10,
    prop2: (param1: string, param2?: number) => "hello",
    prop3: queryMap    
})

Test of Compile-time errors

You can see bellow some compile-time errors beeing catched.


// Ok --> Compile Error: 'prop2' signature is wrong

const queryMap2 = asQueryMap({
    a: asQuery({
        prop1: (param1: Array<string>, param2?: number) => 10,
        prop2: (param1: Array<number>, param2?: number) => "hello",
    })
})


// Ok --> Compile Error: 'prop3' is not of type QueryMap<any>

const query2 = asQuery({
    prop1: (param1: string, param2?: number) => 10,
    prop2: (param1: string, param2?: number) => "hello",
    prop3: 10 // <---- Error !
})

Thank you Cheers


Post a Comment for "Typescript Map Of Arbitrary Generics"