5

I have the following schema columns for a database table (used for Watermelon DB).

const columns = [
  { name: "created_at", type: "number", isOptional: true },
  { name: "created_by", type: "string" },
  { name: "is_corrupt", type: "boolean", isOptional: true },
];

I would like to create a generic which will create the following type from the above example, how can I do that?

type ExpectedInferredTypeFromColumns = {
  created_at: number | null;
  created_by: string;
  is_corrupt: boolean | null;
};

My attempt:

type InferTypeFromColumns<T extends ReadonlyArray<Column>> = {
  [K in T extends ReadonlyArray<infer U>
    ? U extends { name: string }
      ? U["name"]
      : never
    : never]: T extends ReadonlyArray<infer U>
    ? U extends { type: "number"; isOptional: true }
      ? number | null
      : U extends { type: "number" }
      ? number
      : U extends { type: "string"; isOptional: true }
      ? string | null
      : U extends { type: "string" }
      ? string
      : U extends { type: "boolean"; isOptional: true }
      ? boolean | null
      : U extends { type: "boolean" }
      ? boolean
      : never
    : never;
};

type MyInferredType = InferTypeFromColumns<typeof columns>;
// Produces: => 
// type MyInferredType = {
//     created_at: string | number | boolean | null;
//     created_by: string | number | boolean | null;
//     is_corrupt: string | number | boolean | null;
// }

As you can see my attempt doesn't quite meet my ExpectedInferredTypeFromColumns

2 Answers 2

2

There you go: playground

const columns = [
  { name: "created_at", type: "number", isOptional: true },
  { name: "created_by", type: "string" },
  { name: "is_corrupt", type: "boolean", isOptional: true },
] as const; // define as const so `columns[number]` gives precise type inference

type Column = {
  name: string;
  type: "number" | "string" | "boolean"
  isOptional?: boolean
}

type TypeMapper = {
  boolean: boolean;
  string: string;
  number: number;
}

// You need to create a union depending if `isOptional` is defined or not
type InferTypeFromColumns<T extends ReadonlyArray<Column>> = {
  [K in T[number] as K['name']]: TypeMapper[K['type']] | (K['isOptional'] extends true ? null : never)
}

type Test = InferTypeFromColumns<typeof columns>
/*
type Test = {
    created_at: number | null;
    created_by: string;
    is_corrupt: boolean | null;
}
*/

If you need to have the optional keys, you will need to do an intersection of the optional and required keys like this:

type RequiredInferTypeFromColumns<T extends ReadonlyArray<Column>> = {
  [K in T[number] as K['isOptional'] extends true ? never : K['name']]: TypeMapper[K['type']]
}

type OptionalInferTypeFromColumns<T extends ReadonlyArray<Column>> = {
  [K in T[number] as K['isOptional'] extends true ? K['name'] : never]?: TypeMapper[K['type']] | null
}

type Intersection<A, B> = A & B extends infer U
  ? { [P in keyof U]: U[P] }
  : never;

type Test = Intersection<RequiredInferTypeFromColumns<typeof columns>, OptionalInferTypeFromColumns<typeof columns>>
/*
type Test = {
    created_by: string;
    created_at?: number | null | undefined;
    is_corrupt?: boolean | null | undefined;
}
*/

This answer was inspired by this solution: Conditionally apply ? modifier in mapped type per-property

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks, I am struggling with one additional step, perhaps you could give a hint on how to solve. How would you also make those keys optional? ie desired output type Test = { created_at?: number | null; created_by: string; is_corrupt?: boolean | null; } Thanks!
See the edit :)
1

Okay so by basically copying this answer I was able to get a working solution

const columns = [
  { name: "created_at", type: "number", isOptional: true },
  { name: "created_by", type: "string", isOptional: false },
  { name: "is_corrupt", type: "boolean", isOptional: true },
] as const;

type InferTypeFromColumns<P extends readonly unknown[]> = {
  [K in IndexKeys<P> as Name<P[K]>]: Type<P[K]>;
};
type IndexKeys<A extends readonly unknown[]> = Exclude<keyof A, keyof []>;
type Name<O> = O extends { name: infer N }
  ? N extends string
    ? N
    : never
  : never;
type Type<T> = T extends { type: "number"; isOptional: true }
  ? number | null
  : T extends { type: "number" }
  ? number
  : T extends { type: "string"; isOptional: true }
  ? string | null
  : T extends { type: "string" }
  ? string
  : T extends { type: "boolean"; isOptional: true }
  ? boolean | null
  : T extends { type: "boolean" }
  ? boolean
  : never;

type MyInferredType = InferTypeFromColumns<typeof columns>;

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.