将字段值分配给对象不会推断与其字段对应的值类型

Assign field value to an object does not infer value type corresponding its field

Payload<T> 类型具有 fieldNamevalue 属性。 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。我希望当 fieldNamestatus 时,value 类型应该被推断为 'idle' | 'loading' | 'failed'.

TypeScript Playground

你有一个错误,因为 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;
  }
}

Playground


更新

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"

Testab 的联合,这是预期的,这里没有什么新东西。

但是,如果您将另一种类型添加到具有 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 是超类型,BA 的子类型。 您只能使用来自超类型的道具,因为它们总是可以安全获得的。

这就是为什么在您的情况下 value 被评估为字符串,因为将其视为 string 总是更安全。如果你添加条件,就像我在第一个例子中所做的那样 if (fieldName === 'status') TS 将能够推断出 valuefieldName 的类型,否则 TS 将坚持使用最安全的类型。