Home > Mobile >  How to deeply infer types using TypeScript?
How to deeply infer types using TypeScript?

Time:08-04

I am currently struggling with some sort of type inference.

Current State

At the moment, my object looks like this:

const config: Config = {
  calculate({ answers }) {
    answers.questionOne // <-- SHOULD be allowed
    answers.questionUnknown // <-- Should NOT be allowed
  },
  questions: {
    questionOne: {
      title: "Test Title",
      answers: ["answer-1", "answer-2", "answer-3"],
    },
  },
}

For my types, I am having the Config type like this:

// The answers object looks like: { questionKey: answerKey }.
type CalculateCtx = { answers: Record<string, string> }

type Question = {
  title: string;
  answers: string[];
}

type Config = {
  calculate(ctx: CalculateCtx): void;
  questions: Record<string, Question>;
}

Goal State

My goal now, is that I want to be able to type the object in a matter of very strict. I want the calculate function, to only type the answers with the available question keys. So that answers in the calculate function would be typed as { questionOne: "answer-1" | "answer-2" | "answer-3" } and not as { [key: string]: string }.

I know, that this is possible (since libraries like trpc and stitches.js do this stuff), but I find it hard to find out how to properly do this.

Can someone please help me.

CodePudding user response:

Edit:

As noted in the comments, the type of questionOne is not quite right yet. Here is an edit to fix this:

function useConfig<T extends Record<string, string>>(config: Config<T>){}

type CalculateCtx<T extends Record<string, string>> = { answers: {
  [K in keyof T]: T[K]
}}

type Question<V extends string> = {
  title: string;
  answers: V[];
}

type Config<T extends Record<string, string>> = {
  calculate(ctx: CalculateCtx<T>): void;
  questions: {
    [K in keyof T]: Question<T[K]>
  }
}

Playground


Something like this can be achieved when using the config object in a generic function.

function useConfig<T extends Record<string, Question>>(config: Config<T>){}

We use T to store the contents of questions.

type Config<T extends Record<string, Question>> = {
  calculate(ctx: CalculateCtx<T>): void;
  questions: T;
}

Config will have to be generic too where questions is T and the ctx of calculate is CalculateCtx<T>.

For CalculateCtx<T>, we use the keys of T to construct the shape of answers with a new Record.

type CalculateCtx<T> = { answers: Record<keyof T, string> }

You will now get the desired error when passing a config object literal to the function.

const config = useConfig({
  calculate({ answers }) {
    answers.questionOne
    answers.questionUnknown // Error
  },
  questions: {
    questionOne: {
      title: "Test Title",
      answers: ["answer-1", "answer-2", "answer-3"],
    },
  },
})

Playground

CodePudding user response:

An approach using 'as const'. I added Partial<> just to allow 'unanswered' state (with the side effect of the types having additional | undefined.

const QuestionSettings = {
  questionOne: {
    title: "Title 1",
    answers: ["answer-1a", "answer-1b", "answer-1c"],
  },
  questionTwo: {
    title: "Title 2",
    answers: ["answer-2a", "answer-2b"],
  },
} as const;


type AvailableAnsMap = {
  [qName in keyof typeof QuestionSettings]:
  typeof QuestionSettings[qName] extends { answers: Readonly<unknown[]> } ?
  typeof QuestionSettings[qName]['answers'][number] : never
}

type Config = {
  calculate(ctx: { answers: Partial<AvailableAnsMap> }): void;
  questions: typeof QuestionSettings;
}

const config: Config = {
  calculate({ answers }: { answers: Partial<AvailableAnsMap> }) {
    answers.questionOne //"answer-1a" | "answer-1b" | "answer-1c" | undefined
    answers.questionTwo //"answer-2a" | "answer-2b" | undefined
  },
  questions: QuestionSettings
}

let studentAns1: Partial<AvailableAnsMap> = { questionOne: 'answer-1c' };
let studentAns2: Partial<AvailableAnsMap> = { questionTwo: 'answer-2a' };
let studentAns3: Partial<AvailableAnsMap> = { questionOne: 'answer-1a', questionTwo: 'answer-2a' };

config.calculate({ answers: studentAns1 })
  • Related