I have an object that I want to "overwrite" certain types for recursively. This is a contrived example, but in this case, I want to take any type that is a number
and convert it to a string
.
type NumbersStringified<O extends Object> = {
[K in keyof O]: O[K] extends Object ? NumbersStringified<O[K]> : O[K] extends number ? string : O[K];
};
type Original = {
data1: number,
data2: boolean,
data3: string,
data4: {
data5: number,
data6: boolean,
data7: string
}
};
const converted: NumbersStringified<Original> = {
data1: 'a',
data2: false,
data3: '',
data4: {
data5: 'a',
data6: false,
data7: ''
}
};
In this example, I am expecting/wanting data1
and data5
to be strings but TS still thinks they should be a number
. Does anyone know why the pattern of O[K] extends number ? string
does not match data1
and data2
?
Thanks for any help!
CodePudding user response:
You are using Object
instead of object
, so it's actually expecting the literal Object
global. Changing it will make it work:
type NumbersStringified<O extends object> = {
[K in keyof O]: O[K] extends object ? NumbersStringified<O[K]> : O[K] extends number ? string : O[K];
};
CodePudding user response:
The Object
type does not mean what you think it means. It refers to any type that can be indexed into like an object. This includes auto-wrapped primitives. For example, string
extends Object
because, for example, "hello".toUpperCase()
is allowed. Essentially only null
and undefined
do not extend Object
. And that means your NumbersStringified
type is pretty much a no-op; if O[K]
is a number
then O[K] extends Object
and you never even reach the part where number
is replaced with string
.
If you want to refer to non-primitives, you should use the object
type (note the difference in case).
That being said, you could write NumbersStringified
without referring to Object
or object
at all, since mapped types will automatically behave like an identity function for primitives (e.g., string
goes in, string
comes out):
type NumbersStringified<T> = T extends number ? string : {
[K in keyof T]: NumbersStringified<T[K]>
};
This is much simpler and gives you the behavior you want, at least for your example:
type N = NumbersStringified<Original>
/* type N = {
data1: string;
data2: boolean;
data3: string;
data4: {
data5: string;
data6: boolean;
data7: string;
};
} */
const converted: NumbersStringified<Original> = {
data1: 'a',
data2: false,
data3: '',
data4: {
data5: 'a',
data6: false,
data7: ''
}
}; // okay