Type a function that, given another function, returns a similar function as the given function without 1 argument

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

Type a function that, given another function, returns a similar function as the given function without 1 argument

问题

I'm here to help with the translation. Here's the translated code:

我想要创建一个名为`outerFn`的函数,该函数以一个名为`innerFn`的函数作为参数。`innerFn`函数接受一个对象类型的参数,而且该对象将需要具有一个`user`参数。然后,`outerFn`应该返回一个名为`returnFn`的函数,其类型应该与`innerFn`类似,但其参数对象不应该要求`user`参数(但仍然需要所有其他参数)。

我希望这样做是为了让我的`outerFn`能够返回一个`returnFn`,该函数实际上调用`innerFn`,但自动注入`user`(即,无需我发送`user`)。

例如:

```typescript
type User = { user: { id: number, name: string } };

type Place = { Place: string };
type UserAndPlace = User & Place;
type InnerFn1Resolve = { something: string };
type InnerFn1 = (args: UserAndPlace) => Promise<InnerFn1Resolve>;
const innerFn1: InnerFn1 = async (args) => Promise.resolve({ something: 'yes' });
type ReturnFn1 = (args: Place) => Promise<InnerFn1Resolve>;
const returnFn1: ReturnFn1 = outerFn(innerFn1);

type Job = { job: number };
type UserAndJob = User & Job;
type InnerFn2Resolve = { else: number };
type InnerFn2 = (args: UserAndJob) => Promise<InnerFn2Resolve>;
const innerFn2: InnerFn2 = async (args) => Promise.resolve({ else: 1 });
type ReturnFn2 = (args: Job) => Promise<InnerFn2Resolve>;
const returnFn2: ReturnFn2 = outerFn(innerFn2);

我尝试了以下outerFn的实现。上述类型是正确的,但outerFn的返回部分出现了问题。

type ArgsWithUser = Record<string, unknown> & User;
type InnerFn<Args extends ArgsWithUser, Response> = (args: Args) => Promise<Response>
type ReturnFn<Args extends ArgsWithUser, Response> = (args: Omit<Args, 'user'>) => Promise<Response>;

const outerFn = <Args extends ArgsWithUser, Response>(innerFn: InnerFn<Args, Response>): ReturnFn<Args, Response> => {
    const user: User['user'] = {
        id: 1,
        name: 'Something',
    };
    return async (argsWithoutUser) => innerFn({ ...argsWithoutUser, user });
};

我得到了以下关于innerFn参数(即{ ...argsWithoutUser, user })的错误。

参数类型Omit<Args, "user"> & { user: User; }不能分配给类型Args
Omit<Args, "user"> & { user: User; }可以分配给类型Args的约束条件,但Args可以用不同的子类型来实例化。ts(2345)

我真的不理解这个错误。我明白它试图表明返回值可能与Args不完全匹配...但我也不明白为什么会出现这种情况,感觉自己可能在某些方面弄错了类型。

如果不必要的话,我可以将对象强制转换为Args,但我想要避免强制转换(我认为不应该需要强制转换)。

TypeScript Playground重现中,您可以看到这个错误。

我特别想弄清楚的是为什么类型不正确。我已经使用TypeScript超过3年,通常我很明白为什么类型

英文:

I would like to create a function (to be known as outerFn) that is given a function (to be known as innerFn) as an argument. The innerFn function is given an object type argument, and that object will be required to have a user parameter. The outerFn should then return a function (to be known as returnFn) that should have a similar type as innerFn but its argument object should not require the user parameter (but still require all other arguments).

I would like this so that my outerFn can return a returnFn that actually calls the innerFn but injects the user automatically (i.e. without me having to send the user)

ex.

type User = { user: { id: number, name: string } };

type Place = { Place: string };
type UserAndPlace = User &amp; Place;
type InnerFn1Resolve = { something: string };
type InnerFn1 = (args: UserAndPlace) =&gt; Promise&lt;InnerFn1Resolve&gt;;
const innerFn1: InnerFn1 = async (args) =&gt; Promise.resolve({ something: &#39;yes&#39; });
type ReturnFn1 = (args: Place) =&gt; Promise&lt;InnerFn1Resolve&gt;;
const returnFn1: ReturnFn1 = outerFn(innerFn1);

type Job = { job: number };
type UserAndJob = User &amp; Job;
type InnerFn2Resolve = { else: number };
type InnerFn2 = (args: UserAndJob) =&gt; Promise&lt;InnerFn2Resolve&gt;;
const innerFn2: InnerFn2 = async (args) =&gt; Promise.resolve({ else: 1 });
type ReturnFn2 = (args: Job) =&gt; Promise&lt;InnerFn2Resolve&gt;;
const returnFn2: ReturnFn2 = outerFn(innerFn2);

I have tried the following for the outerFn. The typing is correct for the above...but the outerFn return complains.

type ArgsWithUser = Record&lt;string, unknown&gt; &amp; User;
type InnerFn&lt;Args extends ArgsWithUser, Response&gt; = (args: Args) =&gt; Promise&lt;Response&gt;
type ReturnFn&lt;Args extends ArgsWithUser, Response&gt; = (args: Omit&lt;Args, &#39;user&#39;&gt;) =&gt; Promise&lt;Response&gt;

const outerFn = &lt;Args extends ArgsWithUser, Response&gt;(innerFn: InnerFn&lt;Args, Response&gt;): ReturnFn&lt;Args, Response&gt; =&gt; {
    const user: User[&#39;user&#39;] = {
        id: 1,
        name: &#39;Something&#39;,
    };
    return async (argsWithoutUser) =&gt; innerFn({ ...argsWithoutUser, user });
};

I get the following error for the parameters of innerFn (i.e. { ...argsWithoutUser, user })

> Argument of type 'Omit<Args, "user"> & { user: User; }' is not assignable to parameter of type 'Args'.
'Omit<Args, "user"> & { user: User; }' is assignable to the constraint of type 'Args', but 'Args' could be instantiated with a different subtype of constraint 'ArgsWithUser'.ts(2345)

I just...really don't understand the error. I get that it's trying to say that the return value might not exactly match Args...but I also don't see why that would be the case and feel like I must be messing up the types in some way.

I can cast the object as Args but would like to avoid casting if it isn't necessary (and I don't believe it should be).

Here's a TypeScript Playground recreation where you can see the error.

What I am specifically trying to figure out is why the typings are incorrect. I've been working with TypeScript for 3+ years and think I have a pretty good understanding generally on why typings are wrong...but in this instance, I can't seem to wrap my head around why what I have is wrong.

Any help would be much appreciated.

答案1

得分: 1

以下是您要翻译的内容:

"The error you're getting is technically correct. The compiler cannot be sure that the argument type of innerFn will actually accept a general User for its user property. Here is an example of the situation the compiler is worried about:

const innerFn = async (arg: {
user: { id: number, name: "a" | "b" }
}) => ({ a: 1, b: 2 }[arg.user.name]).toFixed(2);

Here innerFn requires that its argument be of type { user: { id: number, name: &quot;a&quot; | &quot;b&quot; } }. This is strictly narrower than User. If you try to call innerFn() with an argument of type User, there's a good chance you'll get a runtime error. If arg.user.name is neither &quot;a&quot; nor &quot;b&quot;, then you will dereference undefined and get a TypeError.

But your typings for outerFn() happily accept innerFn:

const returnFn = outerFn(innerFn); // no compiler error

The type Args is inferred as {user: {id: number; name: &quot;a&quot; | &quot;b&quot;}}, and Omit&lt;Args, &quot;user&quot;&gt; &amp; { user: { id: number; name: string; }; } is just the same as User. But that's not what innerFn needs. That is, the problem is that

// 'Omit<Args, "user"> & { user: { id: number; name: string; }; }' is assignable to the constraint of
// type 'Args', but 'Args' could be instantiated with a different subtype of constraint 'ArgsWithUser'.

And so if you run returnFn() you will get that runtime error, without a compiler warning:

returnFn({}).catch(e => console.log(e)) // no compiler error, but:
// 💥 (intermediate value)[arg.user.name] is undefined

So that's the problem. This is admittedly unlikely, so you might want to just assert (what you called "cast"ing) and move on.


If instead you want to refactor to avoid this problem, you can do so in a way that doesn't run afoul of this by adding User to a type instead of omitting it:

type InnerFn<A, R> = (args: A & User) => Promise<R>
type ReturnFn<A, R> = (args: A) => Promise<R>

const outerFn = <A, R>(innerFn: InnerFn<A, R>): ReturnFn<A, R> => {
const user: User['user'] = {
id: 1,
name: 'Something',
};
return async (argsWithoutUser) => innerFn({ ...argsWithoutUser, user });
};

This compiles with no error and still works for the examples you present. This may or may not actually work for your use case, but at least the compiler is happy."

[Playground link to code](https://www.typescriptlang.org/play?#code/C4TwDgpgBAqgzhATlAvFA3lArgxAuDKASwBMCA7LAWwCMkAaKcgQyogLmESPIHMoAvoIDcAKFEs2cMMwDG0AAqIA9jQA2EKhnGioeqKEhQAgol5wA6kWAALeElRQAShFnLEJADydufRlnIAa3JlAHdyAD4oADJYXDF9A3BoAElyciQAMXJPU3MoCAAPYAhyEjgTM0trO1xGF2llcgQotAAKZiqCPLgASlQopWUqIgRPBrAmlt19Q2gXYCxEcmzcqoLi0vLK8ytbe0R6iEbmiFaoDq6oAHkR4DXzRgByHCQniP6UQZURsYmps7iRJuZrAKDKLAlRDZRwPCpFEplCo9Pa1BjOY6TU4RNo8DLQ8gENL41Y9I4nFq9AgLJYrHJkjEUs4DbSJYFTMGvfBxJAAbReuCeAF1HOgZmy2aQCABGejiiX6STsKBPADKwwgth4vCecoVInliUQmtpUGYcBA5FkF06uxqEOAB0+UTxWXIbUwADpvbbqrYHQd-LhBL1hFAAPThqCeAC0MYMNlGBUQKmQSbcKdcwHlAjE8sjxHSbqgxoAjlgiMaKsxsMHbdRSmCALLXJwAUSgqoUbYAwilMikewnmOQeYggfoQZxCyTR2hzZbrZdeAQxfquaviGQmNQ6IcmKxlQAiZhHqAAHygR5oZ4EOedF0wzBljBoBAATIJebbPVzPUqhV6T1gGUTIiEKCASDad9Qx0dlQRLE1lhhNAHTdXEiwJUMIyjLUKiTOR5DAEoSCgGgQCgEAIWQQxtQqAAzdxwUhN1X0hZioRhGQ4AQOB8yjGsDgMZQZ1YphlDBWxoCodxoGkVwiHoohrTmUSCQ2SBZGAOBPT0NJwSk5BQncco8H4vQnluaw4UYI8uSPKJYkwDdCClHdaCQMMlQ4LhtTDXNBCeYhqx4oheBYdRoBAhNoCnLhmB4MFlHoqBzKSIwnh6XUyPYzKqiCtwsDUUi6ELTgR2AIhmBIqBQhqM0oBIRT6KQRsoDgLAaFU5KoDixAEvIME8rtfZBU9Cc9ALUJE1kGwoA0ZhtmivCoDAFRIqoeVjUWZD3XQAQgNkarZraaAvl

英文:

The error you're getting is technically correct. The compiler cannot be sure that the argument type of innerFn will actually accept a general User for its user property. Here is an example of the situation the compiler is worried about:

const innerFn = async (arg: {
    user: { id: number, name: &quot;a&quot; | &quot;b&quot; }
}) =&gt; ({ a: 1, b: 2 }[arg.user.name]).toFixed(2);

Here innerFn requires that its argument be of type { user: { id: number, name: &quot;a&quot; | &quot;b&quot; } }. This is strictly narrower than User. If you try to call innerFn() with an argument of type User, there's a good chance you'll get a runtime error. If if arg.user.name is neither &quot;a&quot; nor &quot;b&quot; then you will dereference undefined and get a TypeError.

But your typings for outerFn() happily accept innerFn:

const returnFn = outerFn(innerFn); // no compiler error

The type Args is inferred as {user: {id: number; name: &quot;a&quot; | &quot;b&quot;}}, and Omit&lt;Args, &quot;user&quot;&gt; &amp; { user: { id: number; name: string; }; } is just the same as User. But that's not what innerFn needs. That is, the problem is that

//  &#39;Omit&lt;Args, &quot;user&quot;&gt; &amp; { user: { id: number; name: string; }; }&#39; is assignable to the constraint of 
//  type &#39;Args&#39;, but &#39;Args&#39; could be instantiated with a different subtype of constraint &#39;ArgsWithUser&#39;.

And so if you run returnFn() you will get that runtime error, without a compiler warning:

returnFn({}).catch(e =&gt; console.log(e)) // no compiler error, but:
// &#128165; (intermediate value)[arg.user.name] is undefined 

So that's the problem. This is admittedly unlikely so you might want to just assert (what you called "cast"ing) and move on.


If instead you want to refactor to avoid this problem, you can do so in a way that doesn't run afoul of this by adding User to a type instead of omitting it:

type InnerFn&lt;A, R&gt; = (args: A &amp; User) =&gt; Promise&lt;R&gt;
type ReturnFn&lt;A, R&gt; = (args: A) =&gt; Promise&lt;R&gt;

const outerFn = &lt;A, R&gt;(innerFn: InnerFn&lt;A, R&gt;): ReturnFn&lt;A, R&gt; =&gt; {
    const user: User[&#39;user&#39;] = {
        id: 1,
        name: &#39;Something&#39;,
    };
    return async (argsWithoutUser) =&gt; innerFn({ ...argsWithoutUser, user });
};

This compiles with no error, and still works for the examples you present. This may or may not actually work for your use case, but at least the compiler is happy.

Playground link to code

huangapple
  • 本文由 发表于 2023年6月6日 07:30:36
  • 转载请务必保留本文链接:https://go.coder-hub.com/76410579.html
匿名

发表评论

匿名网友

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

确定