1

I have a hard time trying to wrap my head around typescript dynamic (and generic) types.

What I'm trying to accomplish is create a function that returns an object with a specific type where some properties of that object must match whatever parameters are given to that function.

So, basically what I want to happen (pseudo):

const listRequest = createRequest('list', {ids: [1, 2]});

this function should create me an object as follows:

{
  operationVersion: 1,
  protocolVersion: 2,
  operation: 'list', // first param
  list: {            // prop name must match with first param
    ids: [1, 2],     // second param
  }
}

For now, my code looks like:

interface IBaseRequest {
  operationId: number;
  protocolVersion: number;
  operation: string;
  authenticationToken?: string;
}

export type BaseRequest<Operation extends string> = {
  [Prop in keyof IBaseRequest]: IBaseRequest[Prop];
} & Record<Operation, any>;

type CreateRequestType = <T extends string>(operation: string, params: any) => BaseRequest<T>;

export const createRequest: CreateRequestType = <T extends string>(operation: string, params: any) => {

  const req = {
    operation: operation,
    operationId: 1,
    protocolVersion: 2,
  };

  req[operation] = params;
  return req as BaseRequest<T>;
};

Now, when create my request object with:

const listRequest = createRequest('list', {a: 'aa'});

I don't get the intellisense for listRequest.list nor that the type of listRequest being type of BaseRequest<'list'>

And if try to create the request with:

type ListRequest = 'list';
const test = <ListRequest>createRequest('list', {a: 'aa'});

I get an error:

Conversion of type 'BaseRequest<string>' to type '"list"' may be a mistake
because neither type sufficiently overlaps with the other. If this was
intentional, convert the expression to 'unknown' first.ts(2352)

Is there a way to accomplish this with types and generics?

2
  • Is it possible that this doesn't work because it is intended to use IBaseRequest as class and not interface? Commented Sep 23, 2019 at 9:40
  • @sandrooco It really doesn't matter for me how this is done / implemented, as long as the end result is an object of type X<Y> with intellisense :) I'm pretty new to typescript types and generics Commented Sep 23, 2019 at 9:48

1 Answer 1

1

Such a function is difficult to code in TypeScript. It's much simpler and terser with the following alternate version, taking a single "aggregated" input parameter, in your example it would be { list: { a: 'aa' }}.

function createRequestBis<K extends string, T>(payload: { [k in K]: T }) {
    const operation = Object.keys(payload)[0] as K;
    return Object.assign({
        operationVersion: 1,
        protocolVersion: 2,
        operation,
    }, payload);
}

const listRequest = createRequestBis({ list: { a: 'aa' } });
listRequest; // Type { operationVersion: number... } & { list: { a: string } } -> True but ugly!
listRequest.operation; // Type "list" -> OK
listRequest.list.a;    // Type "string" -> OK

It works but the returned type that is inferred is a bit ugly. We can enhance it with a custom utility type that deconstruct+reconstruct an object type:

type Prettify<T> = T extends infer Tb ? { [K in keyof Tb]: Tb[K] } : never;

const header = {
    operationVersion: 1,
    protocolVersion: 2
};

function createRequestPretty<K extends string, T>(payload: { [k in K]: T }) {
    const operation = Object.keys(payload)[0] as K;
    const result = Object.assign({ operation }, header, payload);
    return result as any as Prettify<typeof result>;
}

const listRequest2 = createRequestPretty({ list: { a: 'aa' } });
listRequest2; // Type { operation: "list"; operationVersion: number; protocolVersion: number; list: { a: string } } -> OK
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.