Yogev
Yogev

Reputation: 115

How to return a generic type in TypeScript

What I want to achieve:

  1. A "basic type": Payload, that can be extended by subtypes.
  2. A method: createPayloadWithData, that takes some data and returns a subtype of payload.

What I have tried:

const createGUID = (): string => {
  return "";
};

const callService = (path: string, payload: Payload): void => {
  //
};

type Payload = {
  id: string;
  timeStemp: number;
  asyncCall: boolean;
};

function createPayloadWithData<T extends Payload>(
  id: string,
  asyncCall: boolean,
  data?: {}
): T {
  let ts = Date.now();
  let payload = { id, asyncCall, timeStemp: ts, ...data };
  return payload; "//<--- TS error: { id: string; asyncCall: boolean; timeStemp: number; }' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'Payload
}"

const registerUser = (username: string) => {
  const userId = createGUID();
  const payload = createPayloadWithData(userId, false, { username });
  callService("users", payload);
};

const updateRecords = (records: number[]) => {
  const recordsId = createGUID();
  const payload = createPayloadWithData(recordsId, true, { records });
  callService("records", payload);
};

registerUser("john");
updateRecords([1, 2, 3]);

(In sandbox: https://codesandbox.io/s/ecstatic-bas-97ucu?file=/src/index.ts)

TS throws an error for the return value of createPayloadWithData. Please allow me to say that I know WHY it does not work. I understand the logic and I have read the wonderful explanation here: could be instantiated with a different subtype of constraint 'object'

What I don't understand, is how to do it correctly. Would really appreciate your help. Thanks!

Upvotes: 1

Views: 795

Answers (1)

satanTime
satanTime

Reputation: 13539

The problem here is that generic defined like that: T extends Payload.

It means T can contain more properties but id, timeStemp and async for sure and returned T has to respect it.

type PayloadMore = Payload & {required: boolean};

const result: PayloadMore = createPayloadWithData<PayloadMore>('', false);
// but it won't work because `required` isn't present in the return value.

The way you need to do it is to move data as generic and unite it in the return

const createGUID = (): string => {
  return "";
};

const callService = (path: string, payload: Payload): void => {
  //
};

type Payload = {
  id: string;
  timeStemp: number;
  async: boolean;
};

// optional case
function createPayloadWithData(
    id: string,
    async: boolean
): Payload;
// case with data
function createPayloadWithData<T extends object>(
    id: string,
    async: boolean,
    data: T
): Payload & T;
// implementation
function createPayloadWithData(
  id: string,
  async: boolean,
  data?: {}
): Payload {
  let ts = Date.now();
  let payload = { id, async, timeStemp: ts, ...data };
  return payload;
}

const registerUser = (username: string) => {
  const userId = createGUID();
  const payload = createPayloadWithData(userId, false, { username });
  payload.username // <- now it works
  callService("users", payload);
};

const updateRecords = (records: number[]) => {
  const recordsId = createGUID();
  const payload = createPayloadWithData(recordsId, true, { records });
  payload.id // <- now it works
  payload.records // <- now it works
  callService("users", payload);
};

Upvotes: 3

Related Questions