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)
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.
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