skip to Main Content

I’m trying to read from a data file where everything is stored in arrays, and transfer that to the equivalent Javascript object.

What I want to be able to do with typescript is create a system that automatically reads the arrays, and stores them in corresponding object, but also is able to verify that the types are correct in order to avoid hard to debug errors.

What I’d like to be able to do is something like the following


//Template that will specify types of Typescript, but also allow me to use the keys with javascript
const SaveDataTemplate = {
    id: Number(),
    name: String(),
    someBool: Boolean(),
}

//Type to allow for type-checking the save data input/output
type tSaveData = GetKeyTypesFrom<SaveDataTemplate>; //Theoretically would return constant array type of [number, string, boolean];

//Takes in array from save data and returns key/value object
function fromSaveData(data: tSaveData): typeof SaveDataTemplate {
    const returnObj = {};
    let idx = 0;
    
    for (const key in SaveDataTemplate){
        returnObj[key] = data[idx++];
    }
    
    return returnObj as any;
}

//Takes in object which contains keys which are in SaveDataTemplate and returns array in specific format
function toSaveData(data: typeof SaveDataTemplate): tSaveData {
    const returnArr = [];
    
    for (const key in data){
        returnArr.push(data[key]);
    }
    
    return returnArr as any;
}

In a language like C++ Structs would make this easy, but since Jvascript doesn’t have those it’s turning out to be pretty difficult to find a workaround.

2

Answers


  1. For better or worse, TypeScript does not keep track of the order of keys in object types. This request has been asked and declined multiple times, see microsoft/TypeScript#39701 for example. Indeed this was even introduced as an April Fool’s Day feature announcement at microsoft/TypeScript#58019. The keyof type operator produces an union of key types; similarly you cannot get "the order" of a union as a tuple. There are ways to force TypeScript to divulge its internal order representation of a union, but those are not guaranteed to be what you expect. See How to transform union type to tuple type. This is not a feature of TypeScript and will almost certainly never be.

    You really don’t want key order to matter, in general, otherwise {a: string, b: number} and {b: number, a: string} would be different types, and in the vast majority of cases you don’t want to keep track of when a property was added to an object, or where in the object literal the key appears.

    So TypeScript does not know how to take SaveDataTemplate and produce [number, string, boolean] versus, say, [boolean, number, string] or any other permuation. Similarly it cannot be sure what the for...in loop for (const key in SaveDataTemplate) is going to do. Any approach which relies on key ordering is going to need explicit assertions to tell TypeScript what to expect, and this is fragile. Your toSaveData() function, for example, will do unpredictable things, since there’s no way for TypeScript to notice that you’ve passed in {id:0, name:"", someBool:true} and not {someBool:true, id:0, name:""}. I’d say you should avoid such an approach altogether.


    Instead, you might consider changing your data structure to an inherently ordered type. For example, a const-asserted array literal of key-value pairs:

    const saveDataTemplate =
      [["id", Number()], ["name", String()], ["someBool", Boolean()]] as const;
    
    type SaveDataTemplate = typeof saveDataTemplate
    /* type SaveDataTemplate = readonly [
         readonly ["id", number], 
         readonly ["name", string], 
         readonly ["someBool", boolean]
    ] */
    

    This is enough to construct both a tuple of value types, as desired:

    type GetValueTypesFrom<T extends readonly (readonly [PropertyKey, any])[]> =
      { [I in keyof T]: T[I][1] }
    type TSaveDataValues = GetValueTypesFrom<SaveDataTemplate>;
    // type TSaveDataValues = readonly [number, string, boolean]
    

    and the corresponding object type of your original data structure (what you’d get if you used Object.fromEntries() on the array of key-value pairs:

    type GetObjectTypeFrom<T extends readonly (readonly [PropertyKey, any])[]> =
      { [I in `${number}` & keyof T as T[I][0]]: T[I][1] }
    type TSaveDataObject = GetObjectTypeFrom<SaveDataTemplate>;
    /* type TSaveDataObject = {
        id: number;
        name: string;
        someBool: boolean;
    } */
    

    (Note that I changed the word "key" to "value" in my type names, since [number, string, boolean] is a tuple of property values, not keys.)

    And you can use the array of key-value pairs to implement your functions in a way that’s guaranteed to work no matter what order the object keys happen to be:

    function fromSaveData(data: TSaveDataValues): TSaveDataObject {
      const returnObj: any = {};
      let idx = 0;
    
      for (const [key] of saveDataTemplate) {
        returnObj[key] = data[idx++];
      }
    
      return returnObj;
    }
    
    function toSaveData(data: TSaveDataObject): TSaveDataValues {
      const returnArr: any = [];
      for (const [key] of saveDataTemplate) {
        returnArr.push(data[key])
      }
      return returnArr;
    }
    

    In both cases we’re looping over the "key" elements in the saveDataTemplate array. Those will always be in the same order no matter what. Specifically in toSaveData(), the input data object is not required to have keys in any particular order. As long as it’s a valid TSaveDataObject, the function will serialize the object consistently:

    const obj = fromSaveData([1, "two", true]);
    console.log(obj);
    /* {
      "id": 1,
      "name": "two",
      "someBool": true
    }  */
    
    const keys = toSaveData(obj);
    console.log(keys);
    /* [1, "two", true] */
    
    const otherKeys = toSaveData({someBool: true, id: 1, name: "two"});
    console.log(otherKeys);
    /* [1, "two", true] */
    

    Playground link to code

    Login or Signup to reply.
  2. You could try setting up a basic type map to use when inferring the field type from a template. This is similar to how Zod infers the type of a schema. In your case, the template is a schema.

    type SaveData = z.infer<typeof SaveDataSchema>
    

    You can remove the readonly from the fields by prefixing -readonly. This will make the object mutable.

    type BaseTypeMap = {
      number: number
      string: string
      boolean: boolean
    }
    
    type InferTemplateTypes<T, M extends BaseTypeMap = BaseTypeMap> = {
      -readonly [K in keyof T]: T[K] extends keyof M ? M[T[K]] : never
    }
    

    Here are two separate use cases:

    const useCaseA = (): void => {
      const SaveDataTemplate = {
        id: 'number',
        name: 'string',
        someBool: 'boolean',
      } as const
    
      type SaveData = InferTemplateTypes<typeof SaveDataTemplate>
    
      const saveData: SaveData = {
        id: 123,
        name: 'Example',
        someBool: true,
      }
    
      saveData.someBool = false
    
      console.log(saveData) // { id: 123, name: "Example", someBool: false } 
    }
    
    const useCaseB = (): void => {
      const UserTemplate = {
        age: 'number',
        firstName: 'string',
        isActive: 'boolean',
      } as const
    
      type User = InferTemplateTypes<typeof UserTemplate>
    
      const user: User = {
        age: 30,
        firstName: 'John',
        isActive: true,
      }
    
      user.age = 31
      user.firstName = 'Jane'
      user.isActive = false
    
      console.log(user) // { age: 31, firstName: "Jane", isActive: false } 
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search