英文:
TypeScript "could be instantiated with a different subtype of constraint" in case of function parameters and generics
问题
I can help translate the provided text:
我无法理解这个错误。
我尝试做的是:创建一个React Hook(这里仅为简化起见,只显示为一个函数),该Hook以另一个函数作为参数。该参数函数只能接受一个具有某些特定属性的对象作为自己的参数(例如,分页API调用的页面和pageSize - 可以更多(子类型),不能更少)。
以下是一些说明性的代码:
```typescript
interface MyParams {
page: number;
pageSize: number;
}
interface MyResponse {
count: number;
results: any[];
}
type Options<T extends MyParams, K extends MyResponse> = {
getDataFn: (params: T) => Promise<K>;
setData: (data: K) => void;
};
const elaborate = <T extends MyParams, K extends MyResponse>(
options: Options<T, K>
) => {
return options
.getDataFn({ page: 0, pageSize: 100 }) // Error. Why?!
.then((res) => options.setData(res));
};
elaborate<{}, MyResponse>({ // Error. Expected!
getDataFn: (params) => Promise.resolve({ count: "0", results: [] }), // No error. Why?!
setData: () => {},
});
TS PlayGround链接:https://www.typescriptlang.org/play?ssl=27&ssc=1&pln=1&pc=1#code/JYOwLgpgTgZghgYwgAgLIE8AKcpwLYDOyA3gFACQADnAOYQBcyIArngEbQDcF1dAysABeDJqw5RuAX1KhIsRCgwAlCAUoB7EARRlyCdc3CMW7LhSirmAGzAFGcEOgDaAXSmlSYdJRQB5SmDAmgQAPAAqyBAAHpAgACZEGNi4hAA0yADSkTEQ8YnoKmrBEAB8yAC8JBR0YAAicGBwAGIgjAAU1Cl2yGEAlBVlmFDqeMDaIRkl3OTadQ1w7XHzjBn95WUAburAcVLcpPpaYJFWcGzquJAVyOHZsQloWDj4BOlZ0ff5hRpapW0U6gCQS0jH8gWC4TeJVIazKugsYGYUBAyEB4K0FHIADoavVGi02sRkLwRAAGdIkgTCRgARlJpOQkl6mKxYAAFrk2m0LARYaigcEsbM8XBuaper09h4IKdzpcICFiJJ0spVD9tCVCdUIHN8a1kB1noQ+UMRmMIFieeorBsIITkPpDGBGAAiUku9I86y2RiuRm9VIUYXLA18pWBpncIA
我在第19行(.getDataFn({ page: 1, pageSize: 10 })
)上收到一个错误,错误消息是:“类型' { page: number; pageSize: number; } '的参数不能赋值给类型'T'的参数。
' { page: number; pageSize: number; } '可赋值给'T'的约束条件,但'T'可以实例化为不同的子类型约束'MyParams'。”
因此,泛型T似乎可能不包含page
和pageSize
属性。
但这并不正确,因为我还在第23行收到了一个错误,故意试图制造这种类型错误。错误消息是:“类型' {} '不满足约束' MyParams'。
类型' {} '缺少类型' MyParams '的以下属性:page, pageSize”。
所以我实际上不能调用函数elaborate
并传递不满足约束的泛型T,否则会收到错误(如预期的那样)。
那么,是什么导致第19行出现错误?
另外,我预期第24行(Promise.resolve({ count: "1", results: [] })
)也会出现错误,因为我故意将count设置为字符串而不是数字,这不满足setData: (data: K) => void;
的约束,其中K extends MyResponse
。
感谢所有能够为此提供一些见解的人...
编辑 - 更多上下文:
我希望T可以包含一些其他属性
。
理想情况下,该主要函数应该接受一个dataGetter
并自动处理其分页(代码已排除)。其他属性可能是一些筛选器,例如query: string
(我处理)。它应该可重用于所有分页API,因此可能具有更多或不同的子类型,但page
和pageSize
对所有子类型都是公共的。
更好的代码示例:
interface MyParams {
page: number;
pageSize: number;
}
interface MyResponse {
count: number;
results: any[];
}
type Options<T extends MyParams, K extends MyResponse> = {
getDataFn: (params: T) => Promise<K>;
setData: (data: K) => void;
};
const elaborate = <T extends MyParams, K extends MyResponse>(
options: Options<T, K>,
otherParams: Omit<T, 'page' | 'pageSize'>
) => {
return options
.getDataFn({ page: 0, pageSize: 100, ...otherParams })
.then((res) => options.setData(res));
};
///////////////////////////
type MyAPIParams = {
page: number;
pageSize: number;
query: string;
}
type MyAPIResponse = {
count: number;
results: { name: string, age: number }[];
otherProperty: boolean;
}
const API = {
GET_DATA: (params: MyAPIParams): Promise<MyAPIResponse> => Promise.resolve({ count: 0, results: [], otherProperty: true })
}
elaborate<MyAPIParams, MyAPIResponse>({
getDataFn: API.GET_DATA,
setData: (data) => { console.log(data.results, data.otherProperty) },
}, { query: 'test' });
英文:
I can't wrap my head around this error.
What I'm trying to do: make a React Hook (shown as just a function here for simplicity) that takes another function as argument. That argument-function can only accept as own argument an object that has some specific properties (ex. page and pageSize for paginated API calls - could be more (a subtype), can't be less).
Here is some explicative code:
interface MyParams {
page: number;
pageSize: number;
}
interface MyResponse {
count: number;
results: any[];
}
type Options<T extends MyParams, K extends MyResponse> = {
getDataFn: (params: T) => Promise<K>;
setData: (data: K) => void;
};
const elaborate = <T extends MyParams, K extends MyResponse>(
options: Options<T, K>
) => {
return options
.getDataFn({ page: 0, pageSize: 100 }) // Error. Why?!
.then((res) => options.setData(res));
};
elaborate<{}, MyResponse>({ // Error. Expected!
getDataFn: (params) => Promise.resolve({ count: "0", results: [] }), // No error. Why?!
setData: () => {},
});
I get an error on line 19 (.getDataFn({ page: 1, pageSize: 10 })
), that says: "Argument of type '{ page: number; pageSize: number; }' is not assignable to parameter of type 'T'.
'{ page: number; pageSize: number; }' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'MyParams'."
So it seems that the generic T could somehow NOT CONTAIN the page
and pageSize
properties.
Well, that is not true, because I also get an error on line 23, where I purposely tried to make such a type mistake. The error says: "Type '{}' does not satisfy the constraint 'MyParams'.
Type '{}' is missing the following properties from type 'MyParams': page, pageSize".
So I can't actually call the function elaborate
and pass a generic T that does not satisfy the constraint, otherwise I get an error (as expected).
So, what is causing the error on line 19?
Also, I would expect an error on line 24 (Promise.resolve({ count: "1", results: [] })
), where I purposely set count as "1" (string) instead of number, which doesn't satisfy the constraint setData: (data: K) => void;
where K extends MyResponse
.
Thanks to all who can shed some light on this...
EDIT - MORE CONTEXT:
I want that T may contain some other properties
.
Ideally that main-function should take a dataGetter
and handle its pagination automatically (code excluded). Other properties may be some filters, for example a query: string
(that I handle).
It should be reusable for all paginated API, so it may have more or different subtypes, but page
and pageSize
are common to all.
Better code example:
interface MyParams {
page: number;
pageSize: number;
}
interface MyResponse {
count: number;
results: any[];
}
type Options<T extends MyParams, K extends MyResponse> = {
getDataFn: (params: T) => Promise<K>;
setData: (data: K) => void;
};
const elaborate = <T extends MyParams, K extends MyResponse>(
options: Options<T, K>,
otherParams: Omit<T, 'page' | 'pageSize'>
) => {
return options
.getDataFn({ page: 0, pageSize: 100, ...otherParams })
.then((res) => options.setData(res));
};
///////////////////////////
type MyAPIParams = {
page: number;
pageSize: number;
query: string;
}
type MyAPIResponse = {
count: number;
results: {name: string, age: number}[];
otherProperty: boolean;
}
const API = {
GET_DATA: (params: MyAPIParams): Promise<MyAPIResponse> => Promise.resolve({ count: 0, results: [], otherProperty: true})
}
elaborate<MyAPIParams, MyAPIResponse>({
getDataFn: API.GET_DATA,
setData: (data) => { console.log(data.results, data.otherProperty) },
}, {query: 'test'});
答案1
得分: 2
你遇到这个错误是因为你声明了T extends MyParams
,这并不意味着T
会严格等于MyParams
。T
可能包含一些其他属性;因此,当你尝试将MyParams
传递给getDataFn
时,你会得到错误。
为了解决这个问题,你可以使用断言:
getDataFn({ page: 0, pageSize: 100 } as T)
另一个解决方法是为整个选项添加一个泛型参数。这样做的原因是T
肯定是Options<MyParams, MyResponse>
,而MyParams
和MyResponse
无法被扩展,但是T
可能包含一些额外的属性,这对我们没有问题:
<T extends Options<MyParams, MyResponse>>
最后,你可以移除泛型,只期望Options<MyParams, MyResponse>
:
const elaborate = (options: Options<MyParams, MyResponse>) => {}
之所以在Promise.resolve({ count: "1", results: [] })
中不会出错,仅仅是因为你故意将{}
作为第一个参数传递,这不允许编译器正确地推断类型。如果你传递一个预期的类型而不是{}
,你将会看到错误。
更新
由于你期望附加属性,我们必须进一步调整类型。在更新的代码中,尽管看起来正确,但你会得到相同的错误,T extends MyParams
可能具有不同于任何数字的page
或其他属性,例如page: 1
;因此,当你尝试传递page: 0
时,TypeScript 会报错。
最简单的解决方法是从T
中移除MyParams
并重新分配:
type RemoveMyParams<T extends MyParams> = Omit<T, keyof MyParams> & MyParams;
这个解决方法的原理是,T
扩展了MyParams
,而不是等于MyParams
,并且可能会有更加具体的属性值;因此,我们从T
中移除MyParams
的键,然后将通用的MyParams
添加进去,这将使属性值变成任何数字。
在这里,extends
和&
的区别在于,extends
并不严格等于MyParams
,而使用&
添加MyParams
不是泛型的,这就是为什么T
将具有通用的MyParams
而不是特定的一个。
type Options<T extends MyParams, K extends MyResponse> = {
getDataFn: (params: RemoveMyParams<T>) => Promise<K>;
setData: (data: K) => void;
};
英文:
You get the error because you say that T extends MyParams
, which doesn't mean that T
will be strictly equal to MyParams
. T
may contain some other properties; thus, when you try to pass MyParams
to the getDataFn
you get the error.
To solve it, you either can use the assertion:
getDataFn({ page: 0, pageSize: 100 } as T)
Another solution would be to have a generic parameter for the whole options. It works because T
is definitely Options<MyParams, MyResponse>
and MyParams
and MyResponse
can't be extended, however T
may contain some extra properties, which doesn't bother us:
<T extends Options<MyParams, MyResponse>>
The last thing that you could do is remove generic and just expect Options<MyParams, MyResponse>
:
const elaborate = (options:Options<MyParams, MyResponse>) => {}
The reason why you don't get the error with Promise.resolve({ count: "1", results: [] })
, is simply that you purposely passed {}
as first parameter, which doesn't allow the compiler to infer the types correctly. If you pass a expected type instead of {}
you will see the error.
playground for generic for the whole Options
UPDATE
Since you expect additional properties, we must adjust the types further. In the updated code, even though it seems correct, you get the same error, T extends MyParams
may have page
or other property not as any number, but a specific one like page: 1
; thus, when you try to pass page: 0
, typescript will complain about this.
The easiest way to fix it would be to remove MyParams
from T
and assign it back.
type RemoveMyParams<T extends MyParams> = Omit<T, keyof MyParams> & MyParams;
It works because T
extends MyParams
, not equal to MyParams
and may have more narrowed values for the properties; therefore, we remove the keys of MyParams
from T
and add general MyParams
to it, which will have properties as any number.
The difference between extends
and &
here is that extends
is not strictly equal to MyParams
, and adding MyParams
with &
is not generic, that's why T
will have general MyParams
, not a specific one.
type Options<T extends MyParams, K extends MyResponse> = {
getDataFn: (params: RemoveMyParams<T>) => Promise<K>;
setData: (data: K) => void;
};
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论