Eric Amodio
Eric Amodio

Reputation: 723

TypeScript: Enforce generic inferred type parameter in function

Is there a way to restrict the run call below to be strict about the type allowed by the type parameters specified to the RequestType<>? The return type R seems to work, but RQ isn't strict.

class RequestType<RQ, R> {
    constructor(public readonly method: string) { }
 }

interface FooRequest {
    foo: string;
    bar: string;
}

interface FooResponse {
    foobar: string;
}

const FooRequestType = new RequestType<FooRequest, FooResponse>("foo");

function run<RQ, R>(type: RequestType<RQ, R>, request: RQ): R {
    // real code here
    return {} as R;
}

Here are the calls

const foo1 = run(FooRequestType, {}); // want an error here
const foo2 = run(FooRequestType, {
    foo: "foo" // want an error here
});
const foo3 = run(FooRequestType, {
    foo: "foo",
    bar: "bar",
    baz: "" // error here -- good
});

Here is a link to the TypeScript playgound. Any help is appreciated -- Thanks!

Upvotes: 1

Views: 1794

Answers (1)

Maciek Wawro
Maciek Wawro

Reputation: 1008

From docs: Because TypeScript is a structural type system, type parameters only affect the resulting type when consumed as part of the type of a member.

In your case, RequestType<{},FooResponse> and RequestType<FooRequest, FooResponse> have the exact same structure, so in line

run(FooRequestType, {});

the types are correctly inferred as

run<{},FooResponse>(FooRequestType: RequestType<{}, FooResponse>, {}: {})


One way to deal with that is to add some (bogus if needed) property to RequestType, such that RequestType<RQ1,R> is different from RequestType<RQ2,R>. That property could be

class RequestType<RQ, R> {
    private readonly acc: (req: RQ) => RQ | undefined;
    constructor(public readonly method: string) { }
}

Note that for this particular solution to succeed you need to enable strictFunctionTypes option. Otherwise, (req: FooRequest) => FooRequest will be assignable to (req: {}) => {} and so FooRequestType will still be assignable to RequestType<{}, FooResponse>. You can read more about it here.


A different approach would be to not allow TypeScript to infer the wrong type for request, and instead make it infer the type for requestType (changed from type for readability):

type RequestOf<RT> = RT extends RequestType<infer RQ, any> ? RQ : never;
type ResponseOf<RT> = RT extends RequestType<any, infer R> ? R : never;
function run<RT extends RequestType<any, any>>(
    requestType: RT,
    request: RequestOf<RT>
): ResponseOf<RT> {
    return {} as ResponseOf<RT>;
}

Now, TypeScript will "guess" correctly that RT is RequestType<FooRequest, FooResponse>. Then,

type RequestOf<RT> = RT extends RequestType<infer RQ, any> ? RQ : never;

basically says: if RT is RequestType<RQ, something>, make RequestOf<RT> equal to that RQ. For more on that infer magic, see Type inference in conditional types.

Upvotes: 2

Related Questions