0

Could anybody with a better understanding of TypeScript's type system explain to me why the following does not work?

export const mySchema = {
  user: {
    email: {type: 'string'},
    name: {type: 'string'},
    age: {type: 'number'},
  },
  message: {
    user_id: {type: 'string'},
    text: {type: 'string'},
  },
} as const;

type TypeMap = {
  string: string;
  number: number;
};

// Causes an error
type Instance<TableName extends keyof typeof mySchema> = {
  [Column in keyof typeof mySchema[TableName]]: TypeMap[typeof mySchema[TableName][Column]['type']]; 
};

// Totally fine
type UserInstance = {
  [Column in keyof typeof mySchema['user']]: TypeMap[typeof mySchema['user'][Column]['type']];
};

For context I've been trying to create generic instance types for a const object that represents a database schema.

In TypeScript 3.8.3, I see the following error:

Error:(19, 57) TS2536: Type '"type"' cannot be used to index type '{ readonly user: { readonly email: { readonly type: "string"; }; readonly name: { readonly type: "string"; }; readonly age: { readonly type: "number"; }; }; readonly message: { readonly user_id: { readonly type: "string"; }; readonly text: { ...; }; }; }[TableName][Column]'.

Why can't type be used to index here, even though type always exists?

Despite this, why is everything fine if I hard code TableName instead?

Thanks in advance!

Michael
  • 2,258
  • 1
  • 23
  • 31

1 Answers1

0

I don't have a complete answer, as my idea at a fix ran into a similar problem to the one you had, but I think the workarounds I provide might prove useful if yours is a real problem.

In order to find an answer to your question, I started from the most general type TableName can take. That is keyof typeof mySchema. If we investigate typeof mySchema[TableName], we find it's a non-discriminated union, so keyof this type is never.

From here I branched into two trains of thought.

1. Conditional type

You can add a conditional type, to only take the type when it exists.

type InstanceConditionalUnion<TableName extends keyof typeof mySchema> = {
    [Column in keyof typeof mySchema[TableName]]:
        typeof mySchema[TableName][Column] extends {type: any} ?
        TypeMap[typeof mySchema[TableName][Column]['type']] : never; 
};

Then InstanceConditionalUnion<'user'> is equivalent to UserInstance, but InstanceConditionalUnion<keyof typeof mySchema> is {}.

2. Intersection type

To use something like Instance<keyof typeof mySchema> we can convert the union to an intersection like this:

type InstanceConditionalIntersection<TableName extends keyof typeof mySchema> = {
    [Column in keyof UnionToIntersection<typeof mySchema[TableName]>]:
        UnionToIntersection<typeof mySchema[TableName]>[Column] extends {type: any} ?
        TypeMap[UnionToIntersection<typeof mySchema[TableName]>[Column]['type']] : never;
};

type UnionToIntersection<U> = 
    (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never

With UnionToIntersection taken from this answer.

In this case InstanceConditionalIntersection<keyof typeof mySchema> contains all keys from both user and message

Alas this kind of brings me to a point where my answer doesn't work because InstanceIntersection runs into the same problem as your original type.

type InstanceIntersection<TableName extends keyof typeof mySchema> = {
    [Column in keyof UnionToIntersection<typeof mySchema[TableName]>]:
        TypeMap[UnionToIntersection<typeof mySchema[TableName]>[Column]['type']];
}; 
// this is an error. UnionToIntersection<typeof mySchema[TableName]>[Column]
// doesn't have a 'type' property.
Tiberiu Maran
  • 1,983
  • 16
  • 23