我有一个相当棘手的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
x
和y
不能相互赋值。如果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 }>) => void
的dispatch
。设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
,因为它是不相关的。)
支持操场