简体   繁体   中英

Create new type from generic type

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.

Playground link to code

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM