51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

在 TypeScript 中将 “unknown” 用作泛型参数

英文:

Passing unknown as generic in typescript

问题 {#heading}

我正在使用这个npm包,在其中,除其他内容外,他们使用以下类型(这是该包中类型的简化版本):

export interface APIGatewayProxyStructuredResultV2 {
    statusCode?: number;
    headers?: {
        [header: string]: boolean | number | string;
    };
    body?: string;
    isBase64Encoded?: boolean;
    cookies?: string[];
}
export interface APIGatewayProxyEvent {
    version: string;
    routeKey: string;
    rawPath: string;
    body?: string;
}
type ProxyIntegrationBody<T = unknown> = {
    body?: T;
};
type APIGatewayEventRequestContext = {
    key: string;
}
export type ProxyIntegrationEvent<T = unknown> = Omit<APIGatewayProxyEvent, 'body'> & ProxyIntegrationBody<T>
export type ProxyIntegrationResult = Omit<APIGatewayProxyStructuredResultV2, 'statusCode'> & {
    statusCode?: APIGatewayProxyStructuredResultV2['statusCode'];
};
export interface ProxyIntegrationRoute {
    path: string
    method: 'GET' | 'POST' | 'OPTIONS' | 'PUT';
    action: (
    request: ProxyIntegrationEvent<unknown>,
    context: APIGatewayEventRequestContext
    ) => ProxyIntegrationResult | Promise<ProxyIntegrationResult> | string | Promise<string>
}
export interface ProxyIntegrationConfig {
    routes: ProxyIntegrationRoute[];
}

然后在我的代码中,我尝试使用这些类型,如下所示:

const handler: ProxyIntegrationConfig = {
    routes: [
        {
            path: '/playedGames',
            method: 'POST',
            action: async (request: ProxyIntegrationEvent, context) => {
                return { statusCode: 200, body: '' };
            }
        },
        {
            path: '/playedGames',
            method: 'POST',
            action: async (request: ProxyIntegrationEvent<{ name: string, numbers: string[] }>, context) => {
                return { statusCode: 200, body: '' };
            }
        },
    ]
};

如果我理解泛型的使用正确,我应该能够通过泛型向ProxyIntegrationEvent传递自定义类型,然而,在路由数组的第二个对象中,我得到了以下 TypeScript 错误:

Type '(request: ProxyIntegrationEvent<{ name: string; numbers: string[]; }>, context: APIGatewayEventRequestContext) => Promise<{ statusCode: number; body: string; }>' is not assignable to type '(request: ProxyIntegrationEvent<unknown>, context: APIGatewayEventRequestContext) => string | ProxyIntegrationResult | Promise<...> | Promise<...>'.
Types of parameters 'request' and 'request' are incompatible.
Type 'ProxyIntegrationEvent<unknown>' is not assignable to type 'ProxyIntegrationEvent<{ name: string; numbers: string[]; }>'.
Type 'ProxyIntegrationEvent<unknown>' is not assignable to type 'ProxyIntegrationBody<{ name: string; numbers: string[]; }>'.
Types of property 'body' are incompatible.
Type 'unknown' is not assignable to type '{ name: string; numbers: string[]; } | undefined'.ts(2322)
Untitled-1(29, 5): The expected type comes from property 'action' which is declared here on type 'ProxyIntegrationRoute'

如果我移除泛型并简单地使用(request: ProxyIntegrationEvent, context),就像我在路由数组的第一个对象中所做的那样,问题会得到解决,但这样做毫无意义,因为它不会为request参数提供任何类型信息。

我是不是在使用这些类型时出错了,或者这是包中的某种类型错误?我考虑要分支该项目,因此如果需要的话我可以修复它。 英文:

I'm using this npm package where, among other stuff, they use the following types (this is a simplified version of the types in that package):

export interface APIGatewayProxyStructuredResultV2 {
    statusCode?: number;
    headers?: {
        [header: string]: boolean | number | string;
    };
    body?: string;
    isBase64Encoded?: boolean;
    cookies?: string[];
}
export interface APIGatewayProxyEvent {
    version: string;
    routeKey: string;
    rawPath: string;
    body?: string;
}
type ProxyIntegrationBody&lt;T = unknown&gt; = {
    body?: T;
};
type APIGatewayEventRequestContext = {
    key: string;
}
export type ProxyIntegrationEvent&lt;T = unknown&gt; = Omit&lt;APIGatewayProxyEvent, &#39;body&#39;&gt; &amp; ProxyIntegrationBody&lt;T&gt;
export type ProxyIntegrationResult = Omit&lt;APIGatewayProxyStructuredResultV2, &#39;statusCode&#39;&gt; &amp; {
    statusCode?: APIGatewayProxyStructuredResultV2[&#39;statusCode&#39;];
};
export interface ProxyIntegrationRoute {
    path: string
    method: &#39;GET&#39; | &#39;POST&#39; | &#39;OPTIONS&#39; | &#39;PUT&#39;;
    action: (
    request: ProxyIntegrationEvent&lt;unknown&gt;,
    context: APIGatewayEventRequestContext
    ) =&gt; ProxyIntegrationResult | Promise&lt;ProxyIntegrationResult&gt; | string | Promise&lt;string&gt;
}
export interface ProxyIntegrationConfig {
    routes: ProxyIntegrationRoute[];
}

Then in my code, I'm trying to use the types as follows

const handler: ProxyIntegrationConfig = {
    routes: [
        {
            path: &#39;/playedGames&#39;,
            method: &#39;POST&#39;,
            action: async (request: ProxyIntegrationEvent, context) =&gt; {
                return { statusCode: 200, body: &#39;&#39; };
            }
        },
        {
            path: &#39;/playedGames&#39;,
            method: &#39;POST&#39;,
            action: async (request: ProxyIntegrationEvent&lt;{ name: string, numbers: string[] }&gt;, context) =&gt; {
                return { statusCode: 200, body: &#39;&#39; };
            }
        },
    ]
};

If I understand generics correctly, I should be able to pass a custom type to ProxyIntegrationEvent via generics, however, I'm getting the following typescript error in the second object of the routes array:

Type &#39;(request: ProxyIntegrationEvent&lt;{ name: string; numbers: string[]; }&gt;, context: APIGatewayEventRequestContext) =&gt; Promise&lt;{ statusCode: number; body: string; }&gt;&#39; is not assignable to type &#39;(request: ProxyIntegrationEvent&lt;unknown&gt;, context: APIGatewayEventRequestContext) =&gt; string | ProxyIntegrationResult | Promise&lt;...&gt; | Promise&lt;...&gt;&#39;.
  Types of parameters &#39;request&#39; and &#39;request&#39; are incompatible.
    Type &#39;ProxyIntegrationEvent&lt;unknown&gt;&#39; is not assignable to type &#39;ProxyIntegrationEvent&lt;{ name: string; numbers: string[]; }&gt;&#39;.
      Type &#39;ProxyIntegrationEvent&lt;unknown&gt;&#39; is not assignable to type &#39;ProxyIntegrationBody&lt;{ name: string; numbers: string[]; }&gt;&#39;.
        Types of property &#39;body&#39; are incompatible.
          Type &#39;unknown&#39; is not assignable to type &#39;{ name: string; numbers: string[]; } | undefined&#39;.ts(2322)
Untitled-1(29, 5): The expected type comes from property &#39;action&#39; which is declared here on type &#39;ProxyIntegrationRoute&#39;

If I remove the generic and simply do (request: ProxyIntegrationEvent, context), as I have in the first object of the routes array, it fixes the problem but that defeats the whole purpose because it doesn't give me any typing for the request parameter.

Am I using these types incorrectly or is this some typing bug in the package? I'm thinking of forking that project anyway so I could fix it if needed.

答案1 {#1}

得分: 1

如果您希望ProxyIntegrationConfig只是允许 request 回调参数的类型比unknown更窄,那么您可以随时在各种定义中将unknown更改为any,如下所示:

interface ProxyIntegrationRoute {
    path: string
    method: 'GET' | 'POST' | 'OPTIONS' | 'PUT';
    action: (
        request: ProxyIntegrationEvent<any>, // 编辑
        context: APIGatewayEventRequestContext
    ) => ProxyIntegrationResult | Promise<ProxyIntegrationResult> | string | Promise<string>
}

但这会降低类型安全性,直到当前被拒绝的不安全操作也被接受,尽管它们是不安全的:

const handler: ProxyIntegrationConfig = {
    routes: [
        {
            path: '/playedGames',
            method: 'POST',
            action: async (request: ProxyIntegrationEvent, context) => {
                return { statusCode: 200, body: '' };
            }
        },
        {
            path: '/playedGames',
            method: 'POST',
            action: async (request: 
              ProxyIntegrationEvent<{ name: string, numbers: string[] }>, // 可行
              context) => {
                return { statusCode: 200, body: '' };
            }
        },
    ]
};

declare const a: APIGatewayEventRequestContext
handler.routes[1].action(
    { rawPath: '', routeKey: "", version: "", body: { name: "a", numbers: [] } },
    a); // 可行

handler.routes[1].action(
    { rawPath: '', routeKey: "", version: "", body: { oops: 123 } },
    a); // 可行?

另一方面,如果您希望ProxyIntegrationConfig既允许又跟踪这些更具体的类型,以便验证类型安全性,那么它需要编码到类型中。

这意味着ProxyIntegrationConfig可能应该是泛型的,泛型类型参数为ProxyIntegrationEvent<> 类型的request参数的数组类型,该类型用于routes数组中每个元素的request参数,如下所示:

interface ProxyIntegrationConfig<T extends unknown[] = unknown[]> {
    routes: { [I in keyof T]: ProxyIntegrationRoute<T[I]> };
}

这是一种映射数组类型,将T中的元素数组/元组转换为routes中元素的相应数组/元组。这也意味着ProxyIntegrationRoute需要是泛型的:

interface ProxyIntegrationRoute<T = unknown> {
    path: string
    method: 'GET' | 'POST' | 'OPTIONS' | 'PUT';
    action: (
        request: ProxyIntegrationEvent<T>,
        context: APIGatewayEventRequestContext
    ) => ProxyIntegrationResult | Promise<ProxyIntegrationResult> | string | Promise<string>
}

现在我们可以为您的handler提供准确的类型:

const handler: ProxyIntegrationConfig<[unknown, { name: string, numbers: string[] }]> = {
    routes: [
        {
            path: '/playedGames',
            method: 'POST',
            action: async (request, context) => {
                return { statusCode: 200, body: '' };
            }
        },
        {
            path: '/playedGames',
            method: 'POST',
            action: async (request, context) => {
                return { statusCode: 200, body: '' };
            }
        },
    ]
};

请注意,这实际上并不更难编写,因为您不再需要对request参数进行注释。现在我们可以获得强类型的好处:

handler.routes[1].action(
    { rawPath: '', routeKey: "", version: "", body: { name: "a", numbers: [] } },
    a); // 可行

handler.routes[1].action(
    { rawPath: '', routeKey: "", version: "", body: { oops: 123 } }, // 错误!
    a); 

请注意,也许any真的是可接受的,因为您可以使用满足运算符来避免对handler进行任何注释。但所有这些都极大地取决于对handler的用途,这可能最终超出了此处的范围。

[代码示例链接](https://www.typescriptlang.org/play?target=99&ts=5.1.6#code/FAOwhgtgpgzgDmAxlABAKQK4wC4FUZQCCIAnigN7ArUpQAecA9gE7YoCWI2UzAZkqkIAFAJIBxMNwDuYEkOaM6JAMrZmGRNgzMoAEwBKsDABtsANQBMFKjVs5JWAMKNdUAPwAuFCAwQARjwA3Da21AAWUGCuzDCe1qEJKADaEVE8XjjMnADmALpefoyMxpEgKAA+3r4BzBUomTnBiSgAvk2JhbokcQ0g2e0J7DAAQmAEAGwALACiIIguenGFxaUDofOMANbssD1qOUm5ay0htAwsbJzcfAIowuKSUDJyCkrTAG5QXPGJnzHsjBAGX2fTWtgUGG4AGkoCRgVlQadwWApEJJGF4Y0kTROt1MYjbCdbNgSHBUPJFCQRFwoNlmJIASBhi4SAAeAAqKAAvCgMCBNiBGFIQAA+bk-BK4uLs45rElku6iCTSWQfL7YQwARwwsGwzhpdDYPMozU2sPx-VORJo9CYrBQ8vJrypNLpDMBaq4HP 英文:

If you'd like ProxyIntegrationConfig to just allow the request callback param to be of a type narrower than unknown, then you can always just change unknown to any in various definitions like:

interface ProxyIntegrationRoute {
    path: string
    method: &#39;GET&#39; | &#39;POST&#39; | &#39;OPTIONS&#39; | &#39;PUT&#39;;
    action: (
        request: ProxyIntegrationEvent&lt;any&gt;, // edit
        context: APIGatewayEventRequestContext
    ) =&gt; ProxyIntegrationResult | Promise&lt;ProxyIntegrationResult&gt; | string | Promise&lt;string&gt;
}

But this loosens the type safety until things which are currently rejected for being unsafe are accepted despite being unsafe:

const handler: ProxyIntegrationConfig = {
    routes: [
        {
            path: &#39;/playedGames&#39;,
            method: &#39;POST&#39;,
            action: async (request: ProxyIntegrationEvent, context) =&gt; {
                return { statusCode: 200, body: &#39;&#39; };
            }
        },
        {
            path: &#39;/playedGames&#39;,
            method: &#39;POST&#39;,
            action: async (request: 
              ProxyIntegrationEvent&lt;{ name: string, numbers: string[] }&gt;, // okay
              context) =&gt; {
                return { statusCode: 200, body: &#39;&#39; };
            }
        },
    ]
};
`declare const a: APIGatewayEventRequestContext
handler.routes[1].action(
{ rawPath: &#39;&#39;, routeKey: &quot;&quot;, version: &quot;&quot;, body: { name: &quot;a&quot;, numbers: [] } },
a); // okay
handler.routes[1].action(
{ rawPath: &#39;&#39;, routeKey: &quot;&quot;, version: &quot;&quot;, body: { oops: 123 } },
a); // okay?
`


On the other hand if you want ProxyIntegrationConfig to both allow and keep track of these more specific types so that it can verify type safety, then it needs to be encoded into the type.

That means ProxyIntegrationConfig should maybe be generic in the type of the array of type arguments to the ProxyIntegrationEvent&lt;&gt; type for the request parameters in each element of the routes array. Like this:

interface ProxyIntegrationConfig&lt;T extends unknown[] = unknown[]&gt; {
    routes: { [I in keyof T]: ProxyIntegrationRoute&lt;T[I]&gt; };
}

That's a mapped array type, transforming the array/tuple of elements in T to a corresponding array/tuple of elements of routes. That also means ProxyIntegrationRoute needs to be generic:

interface ProxyIntegrationRoute&lt;T = unknown&gt; {
    path: string
    method: &#39;GET&#39; | &#39;POST&#39; | &#39;OPTIONS&#39; | &#39;PUT&#39;;
    action: (
        request: ProxyIntegrationEvent&lt;T&gt;,
        context: APIGatewayEventRequestContext
    ) =&gt; ProxyIntegrationResult | Promise&lt;ProxyIntegrationResult&gt; | string | Promise&lt;string&gt;
}

So now we can give your handler an accurate type:

const handler: ProxyIntegrationConfig&lt;[unknown, { name: string, numbers: string[] }]&gt; = {
    routes: [
        {
            path: &#39;/playedGames&#39;,
            method: &#39;POST&#39;,
            action: async (request, context) =&gt; {
                return { statusCode: 200, body: &#39;&#39; };
            }
        },
        {
            path: &#39;/playedGames&#39;,
            method: &#39;POST&#39;,
            action: async (request, context) =&gt; {
                return { statusCode: 200, body: &#39;&#39; };
            }
        },
    ]
};

And note that this isn't really any harder to write, since you don't have to annotate the request parameter anymore. And now we get the benefits of strong types:

handler.routes[1].action(
    { rawPath: &#39;&#39;, routeKey: &quot;&quot;, version: &quot;&quot;, body: { name: &quot;a&quot;, numbers: [] } },
    a); // okay
`handler.routes[1].action(
{ rawPath: &#39;&#39;, routeKey: &quot;&quot;, version: &quot;&quot;, body: { oops: 123 } }, // error!
a);
`


Note that maybe any really is acceptable, since you can use the satisfies operator to avoid annotation of handler at all. But all of that depends strongly on the use case for what you do with handler, which is probably ultimately out of scope here.

Playground link to code


赞(0)
未经允许不得转载:工具盒子 » 在 TypeScript 中将 “unknown” 用作泛型参数