I am trying to create a new object type based on a generic type, but my typescript keeps saying it is the incorrect type. For example: I want to convert all the objects properties to string.
-- Edited the example to be more complete Entry type
interface Example {
value1: string;
value2: number;
value3: {
value4: boolean
}
}
// Expected new type
interface NewExample {
value1: string;
value2: string;
value3: {
value4: string
}
}
My current function basicly converts all the objects properties to string: (assume there isnt arrays)
public convertObject<T>(obj: T) {
Object.keys(obj).forEach( prop => {
const value = obj[prop];
if(typeof value === 'object')
obj[prop] = this.convertObject(obj[prop])
else
obj[prop] = 'example'+obj[prop]
})
return obj;
}
Given I apply the function on a defined object, all I want is that every "typeof" of any property be a string.
How can I achieve that?
Edit:
let a = { _number: 1, _obj: {mock: true} };
let b = this.convertObject(a);
b._obj; // This should me an object
b._obj.mock; // This should me a string
type ValueOf<O> = O[keyof O];
function convertObject<O extends Record<string, any>, V extends ValueOf<O>>(
obj: O
): V extends Record<any, any>
? Record<keyof O, Record<keyof V, string>>
: Record<keyof O, string> {
(Object.keys(obj) as Array<keyof O>).forEach((prop) => {
const value = obj[prop];
if (typeof value === 'object') {
obj[prop] = convertObject(obj[prop]) as ValueOf<O>;
} else {
obj[prop] = ('example' + obj[prop]) as ValueOf<O>;
}
});
return obj;
}
const hello: Example = {
value1: 'fdsafd',
value2: 444,
value3: {
value4: true
},
another: {
thing: 3213
}
};
const thing = convertObject(hello);
thing
should correctly be typed there with each value as a string. The only thing was you have to add as
statements, otherwise the TS compiler will complain that the types are incompatible.
You can express this type transformation as follows:
type ConvertObject<T> =
T extends object ? { [K in keyof T]: ConvertObject<T[K]> } : string;
That's a conditional type which checks if its input type T
is an object, and if so, evaluates to mapped type where each property is recursively transformed; and if not, evaluates to just string
. You can verify that it does the right thing to Example
:
type NewExample = ConvertObject<Example>;
/* type NewExample = {
value1: string;
value2: string;
value3: {
value4: string;
};
} */
For the actual implementation of convertObject()
, it's hard/impossible to convince the compiler that what you're doing is type safe (see microsoft/TypeScript#33912 for the current state of affairs with implementing functions whose call signatures are generic conditional types), so you will probably need to use type assertions or the equivalent to loosen the type checking inside the implementation enough to avoid compiler errors.
Here's one way to do it:
function convertObject<T extends object>(obj: T): ConvertObject<T> {
const ret: any = {};
Object.keys(obj).forEach(prop => {
const value = (obj as any)[prop];
ret[prop] = typeof value === 'object' ? convertObject(value) : ('example ' + value);
})
return ret;
}
Note that I've made this a standalone function (and not a class method), and that it does not mutate the passed-in obj
in place, but returns a new object ret
. TypeScript's type system really cannot model the situation where something of type (say) number
changes to a string
. So if you call convertObject(obj)
, the type of obj
seen by the compiler will not be changed, and you run the risk of runtime errors if you actually do change the type of obj
. So I'd recommend avoiding such mutation; if you really need to change the passed in obj
, you should be very careful not to use obj
again afterward.
And also note the type loosening inside the implementation; I treat obj
as any
and make ret
of type any
so that the compiler doesn't complain. This places the burden of maintaining type safety on me (or you), so we should check and double check that convertObject()
actually does produce an output of type ConvertObject<T>
when its input is of type T
.
Let's test to see if it works:
let a = { _number: 1, _obj: { mock: true } };
let b = convertObject(a);
/* let b: {
_number: string;
_obj: {
mock: string;
};
} */
console.log(b._number.toUpperCase()); // EXAMPLE 1
console.log(b._obj.mock.toUpperCase()); // EXAMPLE TRUE
Looks good. The type of b
is seen by the compiler to be exactly what you want, and the implementation works also.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.