Home > Blockchain >  Nested generics: Method not returning requested part of passed type, but only the guard type
Nested generics: Method not returning requested part of passed type, but only the guard type

Time:04-08

I'm trying to create a method getCheckedOptions that returns the selected options from a structure we get from graphql, and so I want it as generic as possible, but this method is not returning TOption[], but QuestionOptionWithKey[] and I don't know if I hit a limitation of typescript or made a mistake somewhere.

I created a simplified version of the code in a sandbox with test code, where all the types seem to be correctly infered, except that on the last line shows the incomplete type is returned.

runnable test code

type ResponseValue = {key: string; value: string};
type Question = {key: string, __typename: string, };

type QuestionHash<TQuestion> = {[key: string]: TQuestion};

interface QuestionnaireWithQuestionsWithKeys<TQuestion> {
  questions: TQuestion[]
};

interface ResponseWithCache<TResponse, TQuestionnaire extends QuestionnaireWithQuestionsWithKeys<TQuestion>, TQuestion> {
  response: TResponse;
  questionnaire: TQuestionnaire;
}

interface QuestionOptionWithKey {
  key: string;
}

interface MultiSelectQuestion<MultiSelectQuestionOption> {
  __typename: 'MultiSelectQuestion';
  key: string;
  options: MultiSelectQuestionOption[];
};

interface TextQuestion {
  __typename: 'TextQuestion';
  key: string;
}

type QuestionWithOptionsWhenSensible<TOption> = (MultiSelectQuestion<TOption> | TextQuestion);

interface QuestionnaireWithQuestionOptionKeys<
  TQuestion extends QuestionWithOptionsWhenSensible<TOption>,
  TOption extends QuestionOptionWithKey,
> {
  questions: TQuestion[];
}

function getCheckedOptions<
  TQuestionnaire extends QuestionnaireWithQuestionOptionKeys<TQuestion, TOption>,
  TQuestion extends QuestionWithOptionsWhenSensible<TOption>,
  TOption extends QuestionOptionWithKey,
>(  
  responseWithCache: ResponseWithCache<ResponseValue[], TQuestionnaire, TQuestion>,
  fieldKey: ResponseValue["key"]
): TOption[] {
  // const question = getQuestion(responseWithCache, fieldKey);
  const question = responseWithCache.questionnaire.questions.find((question) => question.key == fieldKey)
  if (question?.__typename != 'MultiSelectQuestion') throw 'foo';
  return question.options;
}

Edit: Created a simpler version.

Is there a way to make typescript infer the right type on the last line? Seems to me like there should, why else allow TSubitem as a return type?

interface Container<TItem> {
  item: TItem;
};

interface Item<TSubitem> {
  subitem: TSubitem;
}

interface WithKey {
  key: string;
}

function getItem<
  TItem extends Item<any>
>(arg: Container<TItem>): TItem {
  return arg.item;
}

function getSubitem<
  TItem extends Item<TSubitem>,
  TSubitem extends WithKey
>(arg: Container<TItem>): TSubitem {
  const item = getItem(arg)
  return item.subitem;
}

const data  = {
  item: {
    subitem: {
      key: "k",
      value: "foo"
    }
  }
};

console.log(getItem(data)?.subitem.value);
console.log(getSubitem(data)?.value);

CodePudding user response:

This has less to do with the nested generics and more with the similarly scoped generics. You cannot infer generics from one another, and generics are only inferred if they are directly tied to the arguments of the function (OR the return type if explicitly assigned).

function getSubitem<
  TItem extends Item<TSubitem>, //<--TItem is inferred from args,
  TSubitem extends WithKey //<--TSubItem isn't inferred at all
>(args: Container<TItem>): TSubItem //=> WithKey
{...}

Instead you can do this

function getSubitem<
  TItem extends Item<WithKey>
>(args: Container<TItem>){...}

// Or if you really need the old generic shape
function getSubitem2<
  TItem extends Item<TSubitem>, //not used, except for generic override
  TSubitem extends WithKey
>(arg: Container<Item<TSubitem>>): TSubitem {
  const item = getItem(arg)
  return item.subitem;
}

View on TS Playground

In your more complex example, you try to infer TOption from your other generics in a sort of roundabout way. Instead use less generics, and make it a sort of single source of truth.

function getCheckedOptions<
  TOption extends QuestionOptionWithKey,
>(
  responseWithCache: ResponseWithCache<
    ResponseValue[], 
    QuestionnaireWithQuestionOptionKeys<QuestionWithOptionsWhenSensible<TOption>, TOption>, 
    QuestionWithOptionsWhenSensible<TOption>
  >,
  fieldKey: ResponseValue["key"]
): TOption[] {
  // const question = getQuestion(responseWithCache, fieldKey);
  const question = responseWithCache.questionnaire.questions.find((question) => question.key == fieldKey)
  if (question?.__typename != 'MultiSelectQuestion') throw 'foo';
  return question.options;
}

View on TS Playground

More or less, using less generics is better, and the generic should be the deepest type of all the generics to make inference work best.

  • Related