Home > database >  How to map type of property to a constructor type
How to map type of property to a constructor type

Time:12-15

I'm trying to map the type of a property to its corresponding constructor type, so string should become StringConstructor, number should bcome NumberConstructor...

This is what I got so far, but the intellisense shows unknown for both test.name?.type and test.age?.type

interface IPerson {
  name?: string
  age?: number
}

const test = {
  name: { type: String },
  age: { type: Number },
} as { [key in keyof IPerson]: {
  type: IPerson[key] extends string
    ? StringConstructor
    : IPerson[key] extends number
      ? NumberConstructor
      : unknown
}}

console.log(test.name?.type) console.log(test.age?.type)

console.log(test.name?.type)
console.log(test.age?.type)

Url to TypeScript REPL: https://www.typescriptlang.org/fr/play?#code/JYOwLgpgTgZghgYwgAgJIAVoGcD2JkDeAUMsiHALYQD8AXMlmFKAOYnJws30gCuFAI2hEAvkSII8jZJGkBeQu1lh6BGQE8ADhHoBlJq2QiANO047CG7fQBy-IVCOmRHLJYDaAawjrkoZN7qODBomFC4IAC6qkpaFhjYeF4 kcgQAB6QIAAmbozMIGykpNTI gUsAMJSTLwIYDhQ7KT0CeFJgakZWblk9sLFxaV2gtDVIPl1DU2DLci8IJ4gOADuIKJiElI4ADYQAHQ7OCwAFMr75FTU 2BxAJRbE7sHR6fn5te32ndAA

what do I need to change to map the type correctly?

CodePudding user response:

Assuming you have the --strictNullChecks compiler option enabled, then the type of name and age properties of IPerson are not string and number; rather, they are string | undefined and number | undefined. Optional properties automatically have the undefined type added to their domain (if you enable the --exactOptionalPropertyTypes compiler option this is a bit more complicated but it's still true that if you read an optional property it may include undefined). And string | undefined does not extend string, so everything falls through to unknown and you are sad.

Perhaps the most expedient way of dealing with this is then to just test extends string | undefined instead of extends string, since string extends string | undefined is true:

const test: { [K in keyof IPerson]: {
  type: IPerson[K] extends string | undefined ? StringConstructor
  : IPerson[K] extends number | undefined ? NumberConstructor
  : unknown
} }
  = {
  name: { type: String },
  age: { type: Number },
};

/* const test: {
    name?: {
        type: StringConstructor;
    } | undefined;
    age?: {
        type: NumberConstructor;
    } | undefined;
} */

Note that there are more general solutions which will take a property of type string | number and turn it to a value of type StringConstructor | NumberConstuctor; that is, they will distribute your type operation over union types; or solutions which will take a mapping type like [string, StringConstructor] | [number, NumberConstructor] | ... and use it to programmatically translate your types so that you can just add new elements like [boolean, BooleanConstructor] and it would work. But all of these are out of scope for the question as asked, so I won't go into them for this question.

Playground link to code

CodePudding user response:

I'm pretty sure the problem is that the types of IPerson.name and IPerson.age are both optional. So you can't test that the property extends string or number as it actually extends (string | undefined) and (number | undefined). Thus your nested ternary is always going to evaluate to unknown.

To fix this you can change the interface of IPerson to have both properties required

  • Related