Search code examples
jsontypescript

Generating Typescript code which enables accessing undefined properties of a TypeScript object


I'd like to create a type, which enables the access of undefined properties of an object, and assumes the type of any for them, and does this recursively. Let me illustrate the problem with an example. (Simplified and corrected the example as per Darryl Noakes' comment.)

const data = {a: "a", b: {c: 'hey'}}

type Data = typeof data & { [key: string]: any }

const dataOptional: Data = data

// This errors:) as expected, because the property "a" does not have such method
dataOptional.a.nonExistentStringMethod
// This two do not error:) as expected, because d has the correct type of "any"
dataOptional.d
dataOptional.d.nonExistentStringMethod
// This does error:( and that's the problem with this type
dataOptional.b.d

So the problem with the above implementation is that the statement does not apply recursively. An important note is that I have the JSON value of data beforehand, and then I have to produce some code where in the end, I can export the original data value in a way which enables the access of undefined properties as described above.

So the question is, how do I get from a JSON value to some TypeScript code which exports the JSON value in a way in which I can access any undefined property recursively, and the accessed value will have the value of any, while keeping typing data of the (nested or not nested) properties which are defined in the original object.


Solution

  • You can use this utility type:

    type WithAny<T> = {
      [key: string]: any;
    } & {
      [key in keyof T]: T[key] extends object ? WithAny<T[key]> : T[key]
    }
    

    It will recursively intersect any object types in the given type with an index type to add any string key as any. Because the original type is more specific (i.e., narrower), this does not remove the typing for known keys.

    Example:

    const data = {
      a: "a",
      b: {
        c: "hey",
      },
    }
    
    type AnyData = WithAny<typeof data>
    
    const anyData: AnyData = data
    
    anyData.a.foo // Error, expected
    anyData.b.foo // No error, expected
    
    /*
    Pseudo-type of AnyData:
    {
      a: string
      b: {
        c: string
        [key: string]: any
      }
      [key: string]: any
    }
    */
    

    TypeScript playground


    Warning

    This applies the intersection to any object type, which includes objects such as Date, Map, etc. If your data includes these, the intersection will be added to them as well.
    If you have known object types in your data, be sure to update the extends check to exclude them. If you do not know what may be present, know what you are doing before using the code given in this answer.

    Example for handling Dates:

    type WithAny<T> = {
      [key: string]: any
    } & {
      [key in keyof T]: T[key] extends object
        ? T[key] extends Date
          ? T[key]
          : WithAny<T[key]>
        : T[key]
    }
    

    You can continue to nest these checks, placing them where the nested WithAny is.