【问题标题】:TypeScript intersection with generic type, and using PartialTypeScript 与泛型类型的交集,并使用 Partial
【发布时间】:2021-09-02 17:34:29
【问题描述】:

我有一个非常棘手的关于泛型和部分的 TypeScript 问题。

这个问题的游乐场是here

我将使用 React 术语解释这个问题,尽管它与 React 没有直接关系。

有一个自定义的 React 钩子,它基本上使用 useReducer 创建了一个 reducer,它包含一些通用属性,以及一个通用的 data 属性。

让我们假设这是钩子:

interface IState<T> {
    some: string;
    data: T;
}

type Action<T> = { type: 'UPDATE_STATE'; payload: Partial<T> };

const foo = <T>(initalState: T) => {
    const state: IState<T> = { some: 'foo', data: initalState };
    const dispatch = (action: Action<T>) => {
        // do something
    }

    return {state, dispatch};
};

此外,还有另一个自定义钩子,它实际上通过向 state reducer 添加额外的公共属性来扩展第一个:

interface MyBar {
    extraData: string;
}

const bar = <T>(initialState: T) => {
    const {state, dispatch} = foo<T & MyBar>({ ...initialState, extraData: 'some data' });
    
    return { state, dispatch };
}

现在,一个典型的用法是:

const baz = () => {
    const { state, dispatch } = bar<IMyState>({ name: 'mor' });
    console.log(state, dispatch)
}

所以在baz,我的状态对象将是:

{
 "extraData": "some data",
 "some": "foo",
 "data": {
  "name": "mor"
 }
}

到目前为止一切顺利。现在我想做以下更改:

const bar = <T>(initialState: T) => {
    const {state, dispatch} = foo<T & MyBar>({ ...initialState, extraData: 'some data' });
    
    **dispatch({ type: 'UPDATE_STATE', payload: { extraData: 'changed' } });**
    return { state, dispatch };
}

我得到:

Type '{ extraData: "changed"; }' is not assignable to type 'Partial<T & MyBar>'.(2322)
input.ts(7, 42): The expected type comes from property 'payload' which is declared here on type 'Action<T & MyBar>'

我不确定为什么会发生这种情况,因为bar 将泛型类型与额外数据一起传递,我希望foo 能够允许它。

如果有任何建议,我将不胜感激!

【问题讨论】:

  • 在这种情况下您不应该使用| 而不是&amp; 吗?
  • foo&lt;T | MyBar&gt;({ ...initialState, extraData: 'some data' }) 为我解决了这个问题,但需要更深入地了解上下文,如果它有任何业务影响
  • @SamridhTuladhar 不幸的是,情况并非如此,| 不能保证我有来自扩展和通用有效负载的密钥,它可以是任何一个,但不能两者兼有。
  • baz 状态对象应该是{ data: { extraData: string, name: string }, …}。注意extraData 嵌套在data 中。

标签: typescript


【解决方案1】:

在这种情况下,TypeScript 无法对可分配性做出任何假设,因为T 不会被推断为任何类型。它就像一个潘多拉盒子。

考虑这个例子:

interface IState<T> {
  some: string;
  data: T;
}

type Action<T> = { type: 'UPDATE_STATE'; payload: Partial<T> };

const foo = <T,>(initalState: T) => {
  const state = { some: 'foo', data: initalState };
  const dispatch = (action: Action<T>) => { }

  return { state, dispatch };
};

interface MyBar {
  extraData: string;
}

const bar = <T extends Record<string, unknown>,>(initialState: T) => {
  const { state, dispatch } = foo({...initialState, extraData: 'some data' });
  let x: Partial<T>
  let y: { extraData: string; }

  x = y // error
  y = x // error


  // TS is unable to make any assumptions about T
  dispatch({ type: 'UPDATE_STATE', payload: { extraData: 's' } })
  return { state, dispatch };
}

const baz = () => {
  const { state, dispatch } = bar({ name: 'mor', extraData: 's' });
  dispatch({ type: 'UPDATE_STATE', payload: { extraData: 's' } })
  console.log(state, dispatch)
}

Playground

我说的是这部分代码:

  let x: Partial<T>
  let y: { extraData: string; }

  x = y // error
  y = x // error

xy 不能相互分配。如果T 是这个对象{ name: string } 怎么办?那么这种行为是不安全的。因为这些对象没有任何共同点。

我知道,当您调用 bar 时,TS 能够推断类型,但请记住我们不在调用阶段..

此外,主要问题在于静态extraData 属性。它也应该使用泛型参数进行参数化。

考虑这个例子:

interface IState<T> {
  some: string;
  data: T;
}

type Action<T> = { type: 'UPDATE_STATE'; payload: Partial<T> };

const foo = <T,>(initalState: T) => {
  const state = { some: 'foo', data: initalState };
  const dispatch = (action: Action<T>) => { }
  return { state, dispatch };
};

interface MyBar {
  extraData: string;
}

const bar = <T, U>(initialState: T, extraData: U) => {
  const { state, dispatch } = foo({ ...initialState, ...extraData });

  dispatch({ type: 'UPDATE_STATE', payload: { ...extraData } })
  return { state, dispatch };
}

const baz = () => {
  const { state, dispatch } = bar({ name: 'mor' }, { extraData: 's' });
  dispatch({ type: 'UPDATE_STATE', payload: { extraData: 's' } })
  console.log(state, dispatch)
}

Playground

【讨论】:

    【解决方案2】:

    看下面的代码:

    const {state, dispatch} = foo<T & MyBar>({ ...initialState, extraData: 'some data' });
    dispatch({ type: 'UPDATE_STATE', payload: { extraData: 'changed' } });
    

    您必须将Action&lt;T &amp; MyBar&gt; 传递给dispatch,但您只发送了MyBar 类型!所以你有两个选择:

    如果您只想发送extraData: 'changed' 作为参数,请将&amp; 更改为|,或者像这样添加...initialState 作为参数:

    dispatch({ type: 'UPDATE_STATE', payload: { ...initialState, extraData: 'changed' } }); 
    

    PlaygroundLink

    【讨论】:

    • 感谢您的回答,尽管在这种情况下这并不完全正确。对于第一个注释,请注意bar 使用foo,因此对于foo,传递的泛型不应该知道任何其他接口。至于第二个,将&amp; 更改为| 也是不正确的,无论是在技术上还是在理论上,因为有效载荷应该始终包含两者。
    【解决方案3】:

    查看底层类型会发现一个更详细的类型错误:

    const bar = <T>(initialState: T) => {
        const {state, dispatch} = foo<T & MyBar>({ ...initialState, extraData: 'some data' });
        
        type Payload = Parameters<typeof dispatch>[0] extends Action<infer T> ? T : never
        const payload: Payload = { extraData: 'changed' };
        //    ^^^^^^^
    }
    
    Type '{ extraData: string; }' is not assignable to type 'T & MyBar'.
      Type '{ extraData: string; }' is not assignable to type 'T'.
        'T' could be instantiated with an arbitrary type which could be unrelated to '{ extraData: string; }'.(2322)
    

    我认为正在发生的事情是 typescript 试图警告可能进行无意义的类型组合,例如:

    bar(10).state.data.toFixed()
    

    state 现在有一个IState&lt;number &amp; MyBar&gt; 类型,所以toFixed() 调用可以编译,但由于data 实际上是一个对象,它会在运行时引发异常。

    不幸的是,这并不能真正回答您的问题,因为我无法找到任何方法来约束 T 使其编译。

    【讨论】:

      【解决方案4】:

      正如this issueabove 中提到的,这是一个打字稿设计限制

      所以我看到的唯一方法是使用类型断言,像这样

      const bar = <T>(initialState: T) => {
          const {state, dispatch} = foo<T & MyBar>({ ...initialState, extraData: 'some data' });
          const createPayload = (state: Partial<MyBar>) => state as T & MyBar;
          
          dispatch({ type: 'UPDATE_STATE', payload: createPayload({ extraData: 'changed' }) });
          return { state, dispatch };
      }
      

      【讨论】:

        【解决方案5】:

        首先,扩展符号不会导致一个类型是两个(或更多)源类型的简单联合。

        type SpreadTwoObj<O1, O2> = Omit<O1, keyof O2> & O2
        
        type A = { a: number, b: string };
        type B = { b: number, c: string };
        declare const a: A;
        declare const b: B;
        
        type C1 = A & B; // just a union
        type C2 = SpreadTwoObj<A, B>;
        const c1: C1 = { ...a, ...b }; // error
        const c2: C2 = { ...a, ...b };
        

        然后想象第一个类型不是对象字面量。

        type U1 = SpreadTwoObj<{ [K in string]: number }, { a: string }>;
        const u1: U1 = { a: 'aa' } // error
        

        这是因为Omit&lt;{ [K: string]: number; }, "a"&gt; 并没有真正消除键“a”。 (Exclude&lt;string, 'a'&gt; 仍然是 string)。这是 TypeScript 的局限性。对比上面的例子:

        type U2 = SpreadTwoObj<{ [K in 'a']: number }, { a: string }>;
        const u2: U2 = { a: 'aa' } // works
        

        您拥有的通用函数&lt;T&gt;(initialState: T) =&gt; any 允许为参数使用这种模棱两可的对象类型。没有什么可以禁止它。在您的情况下,函数foo({ ...initialState, extraData: 'some data' }) 返回(action: Action&lt;T &amp; { extraData: string }&gt;) =&gt; void 类型的dispatch。让T = { [K in string]: number }。那么T &amp; { extraData: string } 的类型是什么?这是非常不健全的类型:

        type T = { [K in string]: number };
        type D = T & { extraData: string };
        
        const d1: D = { extraData: 'changed' }; // Type 'string' is not assignable to type 'number'
        const d2: D = { extraData: 1 }; // Type 'number' is not assignable to type 'string'
        

        这就是为什么除非硬代码类型假设,否则您的示例将永远无法工作。虽然看起来很乱,但我会做这样的事情:

        const bar = <T>(initialState: T) => {
            const { dispatch } = foo({ ...initialState, extraData: 'some data' });
        
            (dispatch as (action: Action<{
                extraData: string;
            }>) => void)({ type: 'UPDATE_STATE', payload: { extraData: 'changed' } });
            return { dispatch };
        }
        

        (我从示例中删除了state,因为它无关紧要。)

        Supporting playground

        【讨论】:

          猜你喜欢
          • 2021-10-18
          • 2020-07-31
          • 2021-07-01
          • 2017-05-26
          • 2019-05-30
          • 2021-12-06
          • 1970-01-01
          • 2017-08-22
          • 1970-01-01
          相关资源
          最近更新 更多