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]
.