将字段值分配给对象不会推断与其字段对应的值类型
Assign field value to an object does not infer value type corresponding its field
Payload<T>
类型具有 fieldName
和 value
属性。 fieldName
应该是CounterState
类型的属性类型,value
应该是其字段对应的类型
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;
}
}
T0
类型正确:
type T0 = {
fieldName: "id";
value: string;
} | {
fieldName: "status";
value: "idle" | "loading" | "failed";
} | {
fieldName: "email";
value: string;
} | {
fieldName: "password";
value: string;
}
当我使用 nState[fieldName] = value;
将值分配给来自 action.payload
的状态时,出现错误:
Type 'string' is not assignable to type '"idle" | "loading" | "failed"'.(2322)
TSC 没有为 fieldName
值推断出正确的 value
类型。为什么?我该如何解决这个问题?实际上,推断出的 value
类型总是 string
。我希望当 fieldName
是 status
时,value
类型应该被推断为 'idle' | 'loading' | 'failed'
.
你有一个错误,因为 fieldName
可能是一个 status
,它只允许 'idle' | 'loading' | 'failed'
,而你正试图分配更宽的类型 - string
。尝试在每个对象中注释 status
,类型和错误将消失。
您的负载类型本身是正确的:
type T0 = Payload<CounterState>;
但是尝试从这个 union:Payload<CounterState>['value']
中得到一个 value
的类型,你会得到一个字符串。这正是您在这里得到的:const { fieldName, value } = action.payload;
.
TypeScript 可以用 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;
}
但是看起来有点疯狂,不是吗?:D
在这种情况下,我们需要欺骗打字稿:
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;
}
}
更新
But don't know why we should use this trick
考虑这个例子:
type Foo = {
a: 'a'
}
type Bar = {
a: 'b'
}
type Result = Foo | Bar
type Test = Result['a'] // "a" | "b"
Test
是 a
和 b
的联合,这是预期的,这里没有什么新东西。
但是,如果您将另一种类型添加到具有 a: string
的联合中,它将更改 Test
。
type Foo = {
a: 'a'
}
type Bar = {
a: 'b'
}
type Baz = {
a: string
}
type Result = Foo | Bar | Baz
type Test = Result['a'] // string
或者换句话说:"a" | string
的计算结果为 string
,因为 string
是更宽的类型。
TypeScript returns 最常见的类型。请参阅文档 here and here
在上面的例子中,string
是超类型,a
是子类型。当我们有子类型和超类型的联合时,只使用超类型的属性总是更安全。
考虑另一个例子:
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
是超类型,B
是 A
的子类型。
您只能使用来自超类型的道具,因为它们总是可以安全获得的。
这就是为什么在您的情况下 value
被评估为字符串,因为将其视为 string
总是更安全。如果你添加条件,就像我在第一个例子中所做的那样 if (fieldName === 'status')
TS 将能够推断出 value
和 fieldName
的类型,否则 TS 将坚持使用最安全的类型。
Payload<T>
类型具有 fieldName
和 value
属性。 fieldName
应该是CounterState
类型的属性类型,value
应该是其字段对应的类型
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;
}
}
T0
类型正确:
type T0 = {
fieldName: "id";
value: string;
} | {
fieldName: "status";
value: "idle" | "loading" | "failed";
} | {
fieldName: "email";
value: string;
} | {
fieldName: "password";
value: string;
}
当我使用 nState[fieldName] = value;
将值分配给来自 action.payload
的状态时,出现错误:
Type 'string' is not assignable to type '"idle" | "loading" | "failed"'.(2322)
TSC 没有为 fieldName
值推断出正确的 value
类型。为什么?我该如何解决这个问题?实际上,推断出的 value
类型总是 string
。我希望当 fieldName
是 status
时,value
类型应该被推断为 'idle' | 'loading' | 'failed'
.
你有一个错误,因为 fieldName
可能是一个 status
,它只允许 'idle' | 'loading' | 'failed'
,而你正试图分配更宽的类型 - string
。尝试在每个对象中注释 status
,类型和错误将消失。
您的负载类型本身是正确的:
type T0 = Payload<CounterState>;
但是尝试从这个 union:Payload<CounterState>['value']
中得到一个 value
的类型,你会得到一个字符串。这正是您在这里得到的:const { fieldName, value } = action.payload;
.
TypeScript 可以用 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;
}
但是看起来有点疯狂,不是吗?:D
在这种情况下,我们需要欺骗打字稿:
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;
}
}
更新
But don't know why we should use this trick
考虑这个例子:
type Foo = {
a: 'a'
}
type Bar = {
a: 'b'
}
type Result = Foo | Bar
type Test = Result['a'] // "a" | "b"
Test
是 a
和 b
的联合,这是预期的,这里没有什么新东西。
但是,如果您将另一种类型添加到具有 a: string
的联合中,它将更改 Test
。
type Foo = {
a: 'a'
}
type Bar = {
a: 'b'
}
type Baz = {
a: string
}
type Result = Foo | Bar | Baz
type Test = Result['a'] // string
或者换句话说:"a" | string
的计算结果为 string
,因为 string
是更宽的类型。
TypeScript returns 最常见的类型。请参阅文档 here and here
在上面的例子中,string
是超类型,a
是子类型。当我们有子类型和超类型的联合时,只使用超类型的属性总是更安全。
考虑另一个例子:
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
是超类型,B
是 A
的子类型。
您只能使用来自超类型的道具,因为它们总是可以安全获得的。
这就是为什么在您的情况下 value
被评估为字符串,因为将其视为 string
总是更安全。如果你添加条件,就像我在第一个例子中所做的那样 if (fieldName === 'status')
TS 将能够推断出 value
和 fieldName
的类型,否则 TS 将坚持使用最安全的类型。