Home > Blockchain >  Set object to intial value by looping through keys
Set object to intial value by looping through keys

Time:08-08

I have an object that I want to allow to be reset. The type of mockData[key] and cloneDeep(initialState[key]) seems to be Todo | User, so I'm not sure where the intersection in the error is coming from.

const initialState = cloneDeep({
  todos: mockTodos,
  user: mockUser,
});

export const mockData = {
  ...initialState,
  reset: () => {
    keysOf(initialState).forEach((key) => {
      mockData[key] = cloneDeep(initialState[key]); // Type 'User | Todo[]' is not assignable to type 'Todo[] & User'.
    });
  },
};

Full error message:

Type 'User | Todo[]' is not assignable to type 'Todo[] & User'.
  Type 'User' is not assignable to type 'Todo[] & User'.
    Type 'User' is missing the following properties from type 'Todo[]': length, pop, push, concat, and 29 more.ts(2322)

Util keysOf function:

/*
 *  This provides a way of having the response be of the form:
 *    "keyOne" | "keyTwo"
 *  instead of simply being of type string[]
 */
export function keysOf<T extends Object>(obj: T): Array<keyof T> {
  return Array.from(Object.keys(obj)) as Array<keyof T>;
}

Copy/pastable reproducible example:

/*
 *  This provides a way of having the response be of the form:
 *    "keyOne" | "keyTwo"
 *  instead of simply being of type string[]
 */
export function keysOf<T extends Object>(obj: T): Array<keyof T> {
  return Array.from(Object.keys(obj)) as Array<keyof T>;
}

interface Todo {
    id: number
    text: string
}

interface User {
    id: number
    name: string
}

const mockTodos: Todo[] = []
const mockUser: User = {
    id: 0,
    name: "John"
}

const initialState = {
  todos: mockTodos,
  user: mockUser,
};

export const mockData = {
  ...initialState,
  reset: () => {
    keysOf(initialState).forEach((key) => {
      mockData[key] = initialState[key];
    });
  },
};

CodePudding user response:

This is a limitation or missing feature of TypeScript, see microsoft/TypeScript#32693. TypeScript is incomplete in this regard, since it can't verify that the line foo[k] = bar[k] is safe when foo and bar are of compatible types and k is of a union type of compatible keys. It's fundamentally the same issue as in microsoft/TypeScript#30581 but with assignments instead of functions. The compiler checks types and not values, and it does these checks once for each line of code instead of once for each possible narrowing. So because it's unsafe to allow foo[k1] = bar[k2] where k1 and k2 are different variables of the same union type as k, then the compiler sees foo[k] = bar[k] as unsafe. For now this is a limitation.


Until and unless this changed, the workaround approach is to rewrite that assignment so that the key type of k is of a generic type parameter K, and that foo and bar are of identical types (say FooBar). The compiler will allow you to write FooBar[K] to FooBar[K] if K is generic. It's still potentially unsafe, but the compiler allows it.

Here's what it looks like for your example:

type State = typeof initialState;
export const mockData = {
    ...initialState,
    reset: () => {
        keysOf(initialState).forEach(<K extends keyof State>(key: K) => {
            const md: State = mockData; // safely upcast
            md[key] = initialState[key]; // State[K] = State[K] is allowed
        });
    },
};

So the forEach() callback is now generic; key's type changes from keyof State to generic K which is constrained to keyof State; and I widen mockData to State (via a new variable). The line md[key] = initialState[k] is allowed because both sides are seen as being of type State[K].

Playground link to code

  • Related