打字稿强制泛型类型为对象

时间:2021-04-05 16:42:31

标签: typescript aws-lambda

我正在尝试使用泛型类型创建泛型函数,该泛型类型必须属于特定对象类型,不仅仅是任何对象。具体来说,我正在尝试围绕 AWS Lambda 处理程序函数创建一个包装器,并为请求和响应标头正文定义明确的类型。

这是迄今为止我能找到的最佳解决方案:

import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';

export type AsyncHandler = (event: APIGatewayProxyEvent, context: Context) => Promise<APIGatewayProxyResult>;

interface EmptyInterface {
    _?: string;
}

export type RequestQueryParameters = EmptyInterface;
export type RequestHeaders = EmptyInterface;
export type RequestBody = EmptyInterface;

export interface Request<Q extends RequestQueryParameters, H extends RequestHeaders, B extends RequestBody> {
    queryParameters: Q;
    headers: H;
    body?: B;
}

export type ResponseHeaders = EmptyInterface;
export type ResponseBody = EmptyInterface;

export interface Response<H extends ResponseHeaders, B extends ResponseBody> {
    statusCode: number;
    headers?: H;
    body: B;
}

export function createHandler<Q, H, B, RH, RB>(main: (request: Request<Q, H, B>) => Promise<Response<RH, RB>>): AsyncHandler {
    return async (event: APIGatewayProxyEvent, context: Context): Promise<APIGatewayProxyResult> => {
        const response = await main({
            queryParameters: (event.queryStringParameters as unknown) as Q,
            headers: (event.headers as unknown) as H,
            body: event.body ? (JSON.parse(event.body) as B) : undefined,
        });
        return {
            statusCode: response.statusCode,
            headers: response.headers as Record<string, string> | undefined,
            body: JSON.stringify(response.body),
        };
    };
}

这是一个示例用法:

interface TestRequestQueryParameters extends RequestQueryParameters {
    a: string;
}

interface TestRequestHeaders extends RequestHeaders {
    b: string;
}

interface TestRequestBody extends RequestBody {
    c: string;
    d?: string;
    e: number;
}

interface TestResponseHeaders extends ResponseHeaders {
    f: string;
}

interface TestResponseBody extends ResponseBody {
    g: string;
    h?: string;
    i: number;
}

const main = async (
    request: Request<TestRequestQueryParameters, TestRequestHeaders, TestRequestBody>,
): Promise<Response<TestResponseHeaders, TestResponseBody>> => {
    console.log(request.queryParameters.a);
    console.log(request.headers.b);
    console.log(request.body.c);
    console.log(request.body.d);
    console.log(request.body.e);
    return {
        statusCode: 200,
        headers: {
            f: 'test',
        },
        body: {
            g: 'test',
            h: 'test',
            i: 100,
        },
    };
};

export const handler = createHandler(main);

这种方法的问题是:

  1. 我不能强制要求查询参数和标头是字符串。
  2. 所有方法都有一个悬空的 _ 属性。

关于如何改进这一点有什么建议吗?是否有用于包装 AWS Lambda 处理程序的现有库?


编辑:在@jcalz 的帮助下,我使用Q extends Record<keyof Q, string | undefined> 改进了代码。但还有两点我想改进:

  1. createHandler 的类型定义变得非常冗长。有没有办法简化它?
  2. 输入/输出接口中不能有可选字段(例如 b?: string)。我可以通过执行 b: string | undefined 来解决这个问题。但是有没有办法b?: string

这是更新后的示例代码:https://tsplay.dev/WkvlDm

1 个答案:

答案 0 :(得分:1)

对于您希望允许任何内容的类型,只要它是对象而不是 primitive,您可以使用 the object type。对于您的 B 类类型参数应该就是这种情况,您拥有的值(在底部的 tsplay 链接中)仅限制为 any | undefined(即 any,由方式)。

对于您希望确保将值限制为 stringundefined 并且您希望支持可选属性的类型,我会创建一个这样的类型别名:

type OptStrDict<T> = { [K in keyof T]?: string }

您将使用它来recursively constrain您的类型参数,例如 T extends OptStrDict<T>


所以你的 Request 界面变成了,例如:

export interface Request<
    Q extends OptStrDict<Q>,
    H extends OptStrDict<H>,
    B extends object
    > {
    queryParameters: Q;
    headers: H;
    body?: B;
}

我将无需编写包含这些更改的完整代码版本,但您可以通过下面的 Playground 链接验证这是否有效。


请注意,重复使用 OptStrDict 类型别名将减少代码的冗长,尤其是将其缩短为 OSD 或其他内容时。如果您认为 createHandler() 的类型签名在此之后仍然过于冗长,我不知道如何最好地进行;务实地说,无论您做什么,拥有五个受约束的泛型类型参数都会占用一些空间。 ?‍♂️


Playground link to code