TypeScript与泛型类型的交集,并使用Partial



我有一个相当棘手的TypeScript泛型和局部问题。

这个问题的游乐场在这里

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

有一个自定义的React钩子,它基本上创建了一个reducer,使用useReducer,它包含一些常见的属性,以及一个通用的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};
};

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

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能够允许它。

我很感激任何建议!

在这种情况下,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)
}

游乐场

我说的是这部分代码:

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)
}

游乐场

看下面的代码:

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

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

更改&|,如果你想只发送extraData: 'changed'作为你的参数或添加...initialState作为一个参数,像这样:

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

PlaygroundLink

查看底层类型会发现一个稍微冗长的类型错误:

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<number & MyBar>,所以toFixed()调用可以编译,但由于data实际上是一个对象,它在运行时抛出异常。

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

正如在这个问题和上面提到的,这是一个typescript设计限制

所以我认为唯一的方法就是使用类型断言,就像这样

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 };
}

首先,扩展符号不会产生一个类型是两个(或多个)源类型的简单并集。

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<{ [K: string]: number; }, "a">并没有真正消除键"a"。(Exclude<string, 'a'>仍然是string)。这就是TypeScript的局限性。与上面的例子比较:

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

你拥有的泛型函数<T>(initialState: T) => any允许有这样一个模棱两可的参数对象类型。没有什么能阻止它。在本例中,函数foo({ ...initialState, extraData: 'some data' })返回类型为(action: Action<T & { extraData: string }>) => voiddispatch。设T = { [K in string]: number }。那么T & { 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,因为它是不相关的。)

支持操场

最新更新