如何完成这个 TS 函数的声明?

我想封装一个通用的 get 函数:

enum Api {
    allPage = 'https://api.example.com/allPage',
    page = 'https://api.example.com/page/:id',
    comment = 'https://api.example.com/page/:id/comment'
}

type PageComment = { content: string }
type Page = { content: string }

type TMap = {
    [K in Api]: {
        [Api.allPage]: {
            param: never
            query: { limit: number }
            data: Page[]
        },
        [Api.page]: {
            param: { id: number }
            query: never
            data: Page
        },
        [Api.comment]: {
            param: { id: number }
            query: { limit: number }
            data: PageComment
        }
    }[K]
}

其中 param 是必选的 url 参数,query 是可选的查询参数,data 指定 api 的返回类型,我希望能够这样使用 get 函数:

// https://api.example.com/allPage
// 返回 Page[]
get(Api.allPage)

// https://api.example.com/page/12345/comment
// 返回 PageComment
get(Api.comment, { param: { id: 12345 } })

// https://api.example.com/page/12345/comment?limit=20
get(Api.comment, { param: { id: 12345 }, query: { limit: 20 } })

// 错误:url 参数 id 为指定
// 返回 never
get(Api.comment)

这是我现在写的:

function get<T extends Api>(
    api: T,
    { param, query }:
        { param?: TMap[T]["param"], query?: TMap[T]['query'] } = {}
): TMap[T]["data"] {
    // TODO
    throw 'Unimplented'
}

现在可以在 paramnever 时省略第二个参数,但是 param 不是 never 时省略第二个参数没有错误提示。我应该如何改进我的代码?

阅读 1.5k
8 个回答

拿走不谢

type Opts<T extends Api> = TMap[T]['param'] extends never
  ? [opts?: { query?: TMap[T]['query'] }]
  : [opts: { param: TMap[T]['param']; query?: TMap[T]['query'] }]

function get<T extends Api>(api: T, ...args: Opts<T>): TMap[T]['data'] {
  // TODO
  throw 'Unimplented'
}

也可以使用函数重载,一个对外强制类型,一个对内稍微宽松一些

function get<T extends Api>(api: T, ...args: Opts<T>): TMap[T]['data'];
function get(api: Api, opts: {param?: object, query?: object} = {}): any {
  // TODO
  throw 'Unimplented'
}

never的就不用传参,不是never就必须传,没传会提示报错?

代码


理解错了,重新更新一版


不过个人觉得这种我还是喜欢一个接口单独一个方法,每个方法上去单独声明入参和返回值

enum Api {
    allPage = 'https://api.example.com/allPage',
    page = 'https://api.example.com/page/:id',
    comment = 'https://api.example.com/page/:id/comment'
}

type PageComment = { content: string }
type Page = { content: string }

type TMap = {
    [K in Api]: {
        [Api.allPage]: {
            // param: never
            query: { limit: number }
            data: Page[]
        },
        [Api.page]: {
            param: { id: number }
            // query: never
            data: Page
        },
        [Api.comment]: {
            param: { id: number }
            query: { limit: number }
            data: PageComment
        }
    }[K]
}


function get<T extends Api>(
    api: T,
    getParams:Omit< TMap[T],'data'>
): TMap[T]["data"] {
    // TODO
    throw 'Unimplented'
}

// https://api.example.com/allPage
// 返回 Page[]
// https://api.example.com/allPage
// 返回 Page[]
get(Api.allPage)

// https://api.example.com/page/12345/comment
// 返回 PageComment
get(Api.comment, { param: { id: 12345 } })

// https://api.example.com/page/12345/comment?limit=20
get(Api.comment, { param: { id: 12345 }, query: { limit: 20 } })

// 错误:url 参数 id 为指定
// 返回 never
get(Api.comment)

不建议用 never

enum Api {
    allPage = 'https://api.example.com/allPage',
    page = 'https://api.example.com/page/:id',
    comment = 'https://api.example.com/page/:id/comment'
}

type PageComment = { content: string }
type Page = { content: string }

type TMap = {
    [K in Api]: {
        [Api.allPage]: {
            param: never
            query: { limit: number }
            data: Page[]
        },
        [Api.page]: {
            param: { id: number }
            query: never
            data: Page
        },
        [Api.comment]: {
            param: { id: number }
            query: { limit: number }
            data: PageComment
        }
    }[K]
}

// 对于需要 param 的 API
function get<T extends Api>(
    api: T,
    options: TMap[T]['param'] extends never ? never : { param: TMap[T]['param'], query?: TMap[T]['query'] }
): TMap[T]["data"];

// 对于不需要 param 的 API
function get<T extends Api>(
    api: T,
    options?: TMap[T]['param'] extends never ? { query?: TMap[T]['query'] } : never
): TMap[T]["data"];

// 实现
function get<T extends Api>(
    api: T,
    { param, query } = {} as { param?: TMap[T]["param"], query?: TMap[T]['query'] }
): TMap[T]["data"] {
    // TODO
    throw 'Unimplented'
}

// 测试
// 错误:参数“"https://api.example.com/page/:id/comment"”的类型与类型参数“T”不匹配。
//   "https://api.example.com/page/:id/comment" 的类型参数不可赋值给 "https://api.example.com/allPage" 的类型参数。
//     类型 "https://api.example.com/page/:id/comment" 与字符串 "https://api.example.com/allPage" 不具有相同的属性。
get(Api.comment);  // 这会报错

一个函数,提供多种不同的参数方案,这种写法叫做重载
在 TS 中,重载的方法是,把各种情形分开声明,然后再实现一个兼容所有重载的函数

// allPage 属于特例,单独声明
function get(api: Api.allPage): TMap[Api.allPage]["data"];

// 非 appPage 的情形,另行声明
function get<T extends Exclude<Api, Api.allPage>>(
  api: T,
  options: {
    param: TMap[T]["param"];
    query: TMap[T]["query"];
  }
): TMap[T]["data"];

// 下面是具体实现,同时兼容上面两种声明
function get<T extends Api>(
  api: Api,
  options?: {
    param: TMap[T]["param"];
    query: TMap[T]["query"];
  }
) {
  // XXXXXXXXXXX
}
// 实际调用的时候,分别选其中一种参数方案传参即可
get(Api.allPage); // no problem
get(Api.comment); // 报参数错误
👆不过这样写还是有缺陷:单参数Api.comment的情况下,TS 会认为你应该传Api.allPage,而不是让你补一个 options 参数。
我猜 TS 对于重载的识别是参数优先,类型次之,这我就不知道该如何解决了。

你可以试一下

enum Api {
  allPage = 'https://api.example.com/allPage',
  page = 'https://api.example.com/page/:id',
  comment = 'https://api.example.com/page/:id/comment'
}

type PageComment = { content: string }
type Page = { content: string }

type TMap = {
  [K in Api]: {
    [Api.allPage]: {
      param: never
      query: { limit: number }
      data: Page[]
    }
    [Api.page]: {
      param: { id: number }
      query: never
      data: Page
    }
    [Api.comment]: {
      param: { id: number }
      query: { limit: number }
      data: PageComment
    }
  }[K]
}

function get<T extends Api>(api: TMap[T]['param'] extends never ? T : never): TMap[T]['data']
function get<T extends Api>(api: T, options: Partial<Omit<TMap[T], 'data'>>): TMap[T]['data']
function get<T extends Api>(api: T, options?: Omit<TMap[T], 'data'>): TMap[T]['data'] {
  // TODO
  throw 'Unimplented'
}

get(Api.allPage)
get(Api.comment, { param: { id: 1 }, query: { limit: 1 } })
get(Api.comment)

可以参考 ts 内置的 addEventListener 声明

感谢 @gauseen 的灵感

type NoParam = { [K in Api]: TMap[K]["param"] extends never ? K : never }[Api];
type HasParam = { [K in Api]: TMap[K]["param"] extends never ? never : K }[Api];

// 不需要参数
function get<
  T extends NoParam,
  Q extends TMap[T]["query"],
  D extends TMap[T]["data"]
>(api: T, opts?: { query?: Q }): D;

// 需要参数
function get<
  T extends HasParam,
  P extends TMap[T]["param"],
  Q extends TMap[T]["query"],
  D extends TMap[T]["data"]
>(api: T, opts: { query?: Q; param: P }): D;

// 实现
function get<
  T extends Api,
  P extends TMap[T]["param"],
  Q extends TMap[T]["query"],
  D extends TMap[T]["data"]
>(api: T, opts?: { query?: Q; param?: P }): D {
  throw "Unimplemented";
}

不过还是有一些小瑕疵,当需要 param 的时候应该提示缺少第二个参数。

// Error: Argument of type 'Api.comment' is not assignable to parameter of type NoParam
// 我希望能提示:函数需要两个参数,缺少第二个参数
get(Api.comment)
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
logo
Microsoft
子站问答
访问
宣传栏