TypeScript在函数参数和泛型情况下可能会实例化为不同子类型的约束。

huangapple go评论73阅读模式
英文:

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似乎可能不包含pagepageSize属性。

但这并不正确,因为我还在第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,因此可能具有更多或不同的子类型,但pagepageSize对所有子类型都是公共的。

更好的代码示例:

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

Playground: https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgLIE8AKcpwLYDOyA3gFACQADnAOYQBcyIArngEbQDcF1dAysABe

英文:

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&lt;T extends MyParams, K extends MyResponse&gt; = {
	getDataFn: (params: T) =&gt; Promise&lt;K&gt;;
	setData: (data: K) =&gt; void;
};

const elaborate = &lt;T extends MyParams, K extends MyResponse&gt;(
	options: Options&lt;T, K&gt;
) =&gt; {
	return options
		.getDataFn({ page: 0, pageSize: 100 }) // Error. Why?!
		.then((res) =&gt; options.setData(res));
};

elaborate&lt;{}, MyResponse&gt;({ // Error. Expected!
	getDataFn: (params) =&gt; Promise.resolve({ count: &quot;0&quot;, results: [] }), // No error. Why?!
	setData: () =&gt; {},
});

TS PlayGround link: https://www.typescriptlang.org/play?ssl=27&amp;ssc=1&amp;pln=1&amp;pc=1#code/JYOwLgpgTgZghgYwgAgLIE8AKcpwLYDOyA3gFACQADnAOYQBcyIArngEbQDcF1dAysABeDJqw5RuAX1KhIsRCgwAlCAUoB7EARRlyCdc3CMW7LhSirmAGzAFGcEOgDaAXSmlSYdJRQB5SmDAmgQAPAAqyBAAHpAgACZEGNi4hAA0yADSkTEQ8YnoKmrBEAB8yAC8JBR0YAAicGBwAGIgjAAU1Cl2yGEAlBVlmFDqeMDaIRkl3OTadQ1w7XHzjBn95WUAburAcVLcpPpaYJFWcGzquJAVyOHZsQloWDj4BOlZ0ff5hRpapW0U6gCQS0jH8gWC4TeJVIazKugsYGYUBAyEB4K0FHIADoavVGi02sRkLwRAAGdIkgTCRgARlJpOQkl6mKxYAAFrk2m0LARYaigcEsbM8XBuaper09h4IKdzpcICFiJJ0spVD9tCVCdUIHN8a1kB1noQ+UMRmMIFieeorBsIITkPpDGBGAAiUku9I86y2RiuRm9VIUYXLA18pWBpncIA

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: &quot;1&quot;, results: [] })), where I purposely set count as "1" (string) instead of number, which doesn't satisfy the constraint setData: (data: K) =&gt; 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&lt;T extends MyParams, K extends MyResponse&gt; = {
	getDataFn: (params: T) =&gt; Promise&lt;K&gt;;
	setData: (data: K) =&gt; void;
};

const elaborate = &lt;T extends MyParams, K extends MyResponse&gt;(
	options: Options&lt;T, K&gt;,
    otherParams: Omit&lt;T, &#39;page&#39; | &#39;pageSize&#39;&gt;
) =&gt; {
	return options
		.getDataFn({ page: 0, pageSize: 100, ...otherParams })
		.then((res) =&gt; 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&lt;MyAPIResponse&gt; =&gt; Promise.resolve({ count: 0, results: [], otherProperty: true})
}

elaborate&lt;MyAPIParams, MyAPIResponse&gt;({
	getDataFn: API.GET_DATA,
	setData: (data) =&gt; { console.log(data.results, data.otherProperty) },
}, {query: &#39;test&#39;});

Playground: https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgLIE8AKcpwLYDOyA3gFACQADnAOYQBcyIArngEbQDcF1dAysABeDJqw5RuAX1KlQkWIhQYAShAKUA9iAIoy5BBubhGLdlwpQ1zADZgCjOCHQBtALpSZYdJRQB5SmDAWgQAPAAqyBAAHpAgACZEGNi4hAA0yADSkTEQ8YnoqurBEAB8yAC8JBR0YAAicGBwAGIgjAAU1Cn2yGEAlBVlmFAaeMA6IRkl3OQ6dQ1w7XHzjBn95WUAbhrAcVLcpAbaYJHWcGwauJAVyOHZsQloWDj4BOlZ0ff5hZrapW0UGgCQW0jH8gWC4TeJVSpGQcOQGjAAAtoMkXqDRmBIcgAOS8CA45AAH1x+IEwhxJVIazKeksYGYUBACKBwQo5AAdDV6o0Wm1iMh8YwAAzpMlCEQARmFouQHPliJRUDRhGQkl67I5yNybTalgINJZ4O0HNmPLgerUvV6exkAHoHY6nc6XS7PN4lOgAIKYACSKqIlTI8MFtBEpnE3BD4uEJjE5hDAEdmNB0IwCGAoKAaB5SF4fI8fb7vsVrsH4QYjGA42YJLD4fqbHZGMQQPgRBmsyAaOkwzXxJI3FH4YrUcMfFAvIxzhprBBHLnDhnkEWy-W4QBxACiYQA+rUvWEve1OujC36A71GEMRmMICEMEWS78yutkDfRjoOfrZxsIPzkErYxkFlRtbG6Nx0lHZVx2gKdkEzFN1VIaRSAgU5zkue9HwvZ40nPYs1B+HQSn5aoIDmXlWhXP0OW3PcDyPGEZgo81FnmQ0BSXWcIA5awNBoNolkab8rHA9JhLgDloJvCcvH6SQYUUkhk1TRgcUgDMcXVTggA

答案1

得分: 2

你遇到这个错误是因为你声明了T extends MyParams,这并不意味着T会严格等于MyParamsT可能包含一些其他属性;因此,当你尝试将MyParams传递给getDataFn时,你会得到错误。

为了解决这个问题,你可以使用断言:

getDataFn({ page: 0, pageSize: 100 } as T)

另一个解决方法是为整个选项添加一个泛型参数。这样做的原因是T肯定是Options<MyParams, MyResponse>,而MyParamsMyResponse无法被扩展,但是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&lt;MyParams, MyResponse&gt; and MyParams and MyResponse can't be extended, however T may contain some extra properties, which doesn't bother us:

&lt;T extends Options&lt;MyParams, MyResponse&gt;&gt;

The last thing that you could do is remove generic and just expect Options&lt;MyParams, MyResponse&gt;:

const elaborate = (options:Options&lt;MyParams, MyResponse&gt;) =&gt; {}

The reason why you don't get the error with Promise.resolve({ count: &quot;1&quot;, 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&lt;T extends MyParams&gt; = Omit&lt;T, keyof MyParams&gt; &amp; 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 &amp; here is that extends is not strictly equal to MyParams, and adding MyParams with &amp; is not generic, that's why T will have general MyParams, not a specific one.

type Options&lt;T extends MyParams, K extends MyResponse&gt; = {
getDataFn: (params: RemoveMyParams&lt;T&gt;) =&gt; Promise&lt;K&gt;;
setData: (data: K) =&gt; void;
};

playground

huangapple
  • 本文由 发表于 2023年5月17日 06:39:18
  • 转载请务必保留本文链接:https://go.coder-hub.com/76267505.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定