The Payload<T> type have fieldName and value properties. The fieldName should be the property type of the CounterState type and the value should be the type corresponding to its field.
type PayloadAction<P = void, T extends string = string, M = never, E = never> = {
payload: P;
type: T;
} & ([M] extends [never] ? {} : {
meta: M;
}) & ([E] extends [never] ? {} : {
error: E;
})
export interface CounterState {
id: string;
status: 'idle' | 'loading' | 'failed';
email: string;
password: string;
}
type Payload<T> = {
[K in keyof T]: {
fieldName: K;
value: T[K];
};
}[keyof T];
type T0 = Payload<CounterState>;
const initialState: CounterState = {
id: '',
email: '',
password: '',
status: 'idle',
};
function reducer(state: CounterState = initialState, action: PayloadAction<Payload<CounterState>>) {
switch (action.type) {
case 'UPDATE_FIELD':
const nState = { ...state };
const { fieldName, value } = action.payload;
nState[fieldName] = value; // TSC throws error
return nState;
default:
return state;
}
}
The T0 type is correct:
type T0 = {
fieldName: "id";
value: string;
} | {
fieldName: "status";
value: "idle" | "loading" | "failed";
} | {
fieldName: "email";
value: string;
} | {
fieldName: "password";
value: string;
}
When I assign the value to state from action.payload use nState[fieldName] = value;, got error:
Type 'string' is not assignable to type '"idle" | "loading" | "failed"'.(2322)
TSC does not infer correct value type for fieldName value. Why? How can I solve this? Actually, the value type inferred is always string. I expect that when the fieldName is status, the value type should be inferred to 'idle' | 'loading' | 'failed'.
CodePudding user response:
You have an error because fieldName might be a status which is allowed to be only 'idle' | 'loading' | 'failed' whereas you are trying to assign much wider type - string. Try to comment status in each object and type and error will disappear.
Your type of payload itself is correct:
type T0 = Payload<CounterState>;
but try to get a type of value from this union:Payload<CounterState>['value'], you will get a string. This is exactly what you are getting here: const { fieldName, value } = action.payload;.
TypeScript is able to infer it with condition statement:
function reducer(state: CounterState = initialState, action: PayloadAction<Payload<CounterState>>) {
switch (action.type) {
case 'UPDATE_FIELD':
const nState = { ...state };
const { fieldName, value } = action.payload;
if (fieldName === 'status') {
nState[fieldName] = value;
} else {
nState[fieldName] = value;
}
return nState;
default:
return state;
}
but is looks a bit crazy, is not it ?:D
In this case we need to trick typescript:
type PayloadAction<P = void, T extends string = string, M = never, E = never> = {
payload: P;
type: T;
} & ([M] extends [never] ? {} : {
meta: M;
}) & ([E] extends [never] ? {} : {
error: E;
})
export interface CounterState {
id: string;
status: 'idle' | 'loading' | 'failed';
email: string;
password: string;
}
type Payload<T> = {
[K in keyof T]: {
fieldName: K;
value: T[K];
};
}[keyof T];
type T0 = Payload<CounterState>;
const initialState: CounterState = {
id: '',
email: '',
password: '',
status: 'idle',
};
const setProperty = <
S extends CounterState,
Prop extends keyof S,
Value extends S[Prop]
>(state: S, prop: Prop, value: Value) => ({
...state,
[prop]: value
})
function reducer(state: CounterState = initialState, action: PayloadAction<Payload<CounterState>>) {
switch (action.type) {
case 'UPDATE_FIELD':
const { fieldName, value } = action.payload;
return setProperty(state, fieldName, value)
default:
return state;
}
}
UPDATE
But don't know why we should use this trick
Consider this example:
type Foo = {
a: 'a'
}
type Bar = {
a: 'b'
}
type Result = Foo | Bar
type Test = Result['a'] // "a" | "b"
Test is a union of a and b it is expected and nothing new here.
However, if you add another one type to a union with a: string it will change Test.
type Foo = {
a: 'a'
}
type Bar = {
a: 'b'
}
type Baz = {
a: string
}
type Result = Foo | Bar | Baz
type Test = Result['a'] // string
Or in other words: "a" | string evaluates to string since string is much wider type.
TypeScript returns the most common type. See docs here and here
In above case string is a super type and a is a subtype. When we have a union of subtype and supertype it is always safer to use only properties from supertype.
Consider another one example:
type A = {
a: 'a'
}
type B = {
a: 'a',
b: 'b'
}
type Result = A | B
type Test = Result['a'] // ok
type Test2 = Result['b'] // error
A is a supertype and B is a subtype of A.
You are only allowed to use props from supertype because thay are always safe to obtain.
This is why in your case value is evaluated as a string, because it is always safer to consider it as a string. If you add condition, like I did in my first example if (fieldName === 'status') TS will be able to infer type of value and fieldName, otherwise TS will stick with most safer types.
