代码之家  ›  专栏  ›  技术社区  ›  Abdulkerim Awad

带有泛型的Typescript函数重载无法按预期工作

  •  0
  • Abdulkerim Awad  · 技术社区  · 3 月前

    我正在尝试创建一个连接到数据库并查询汽车详细信息的函数。我需要这个函数有3个重载,第一个重载返回cars表中存在的基本汽车详细信息,而不将另一个表连接到它,第二个重载返回基本数据+通过外键从其他表中获得的数据,第三个重载返回连接后表的自定义字段。

    经过一番思考,我写了以下代码(这段代码是真实代码的模拟器):

    type t_car_base = {
      carID: number,
      model: string,
      color: string,
      registratinPlateID: number
    }
    
    type t_car_joined = t_car_base & {
      registrationPlateNumber: string
    }
    
    export type t_findOne = {
      (carID: number): Promise<t_car_base>
      (carID: number, joinEntities?: true): Promise<t_car_joined>
      <K extends keyof t_car_joined>(carID: number, fields?: K[]): Promise<Pick<t_car_joined, K>>
    }
    
    const findOne: t_findOne = <K extends keyof t_car_joined>(
      carID: number,
      joinEntities?: true,
      fields?: K[]
    ) => {
      return new Promise<t_car_base | t_car_joined | Pick<t_car_joined, K>>((resolve, reject) => {
        if (fields) {
          resolve({} as Pick<t_car_joined, K>)
        } else if (joinEntities) {
          resolve({} as t_car_joined)
        } else {
          resolve({} as t_car_base)
        }
      })
    }
    
    findOne(1).then((res) => res.color) // Expected to get access to the basic details but got to the full details
    findOne(1, true).then((res) => res.registrationPlateNumber) // Expected to get access to the full details
    findOne(1, ['model']).then((res) => res.model) // Expected to get access to custom fields
    

    但我收到以下错误消息:

    键入“选择<t_car_joined,任何>类型“t_car_base”缺少以下属性:carID、model。。。

    我只需通过以下方式即可获取全部细节 carID 论证,而不是只访问基本细节的能力

    1 回复  |  直到 3 月前
        1
  •  1
  •   jcalz    3 月前

    你真正需要做的就是让它工作 对于来电者 不是为了制造 joinEntities fields optional parameters :

    export type t_findOne = {
      (carID: number): Promise<t_car_base>
      (carID: number, joinEntities: true): Promise<t_car_joined>
      <K extends keyof t_car_joined>(carID: number, fields: K[]): Promise<Pick<t_car_joined, K>>
    }
    
    findOne(1).then((res) => res.color)
    //               ^? (parameter) res: t_car_base
    findOne(1, true).then((res) => res.registrationPlateNumber)
    //                     ^? (parameter) res: t_car_joined
    findOne(1, ['model']).then((res) => res.model) 
    //                          ^? (parameter) res: Pick<t_car_joined, "model">
    

    理想情况下,当你写作时 overloads ,所有呼叫签名都不应重叠。也就是说,您希望没有函数调用与多个调用签名匹配。如果一个调用确实匹配了多个调用签名,那么TypeScript必须做出决定 哪一个 使用时,通常根据它们出现的顺序。。。但也有其他的启发式方法,比如有时TypeScript会选择“最窄”或“最具体”的重载(这有时会让人感到惊讶,根据 microsoft/TypeScript#39833 ). 有时重叠是不可避免的,但你 应该 在可行的情况下避免它。

    在你的情况下,当 join实体 领域 是可选的,然后是类似的调用 findOne(1) 比赛 全部的 您的通话签名。看起来TypeScript出于某种原因决定解析对第二个调用签名的调用。也许这是语言中的错误?但我们不需要担心,因为我们只需设置所需的参数就可以修复它。毕竟,您已经有了一个处理 findOne(1) 案例;你不需要另外两个调用签名来处理它,因为这只会给TypeScript一个机会去做你不想让它做的事情 findOne(1, true) 可以 只有 匹配返回的签名 t_car_joined ,以及 findOne(1, [⋯]) 可以 只有 匹配返回的签名 Pick<t_car_joined, K> 没有歧义,一切正常。


    现在, 为了实施 一个主要问题是,它显然需要 参数,但调用者最多只能传递两个。参数名称无关紧要,JavaScript不会通过以下方式将参数与参数匹配 名称 ,只有通过 位置 .TypeScript,通过扩展,具有相同的功能。所以第三个 领域 实现中的参数是无用的。你需要把它改成这样

    <K extends keyof t_car_joined>(
      carID: number,
      joinEntitiesOrFields?: true | K[],
    ) => {
      return new Promise<t_car_base | t_car_joined | Pick<t_car_joined, K>>((resolve, reject) => {
        if (Array.isArray(joinEntitiesOrFields)) {
          resolve({} as Pick<t_car_joined, K>)
        } else if (joinEntitiesOrFields) {
          resolve({} as t_car_joined)
        } else {
          resolve({} as t_car_base)
        }
      })
    }
    

    TypeScript中的重载是 erased 以及编译为JavaScript时类型系统的其余部分。有人提议让重载在编译为JavaScript时做一些特殊的事情,比如 microsoft/TypeScript#3442 ,但这些都被拒绝了(见 this comment )因为它超出了TypeScript的范围。TypeScript不想成为JavaScript的语法糖。

    从技术上讲,这超出了你所问问题的范围。但如果你希望你的代码在运行时能够正常工作,这一点非常重要。


    至于将函数实现分配给时出现的错误 findOne ,这是因为TypeScript没有也不能根据其调用签名正确验证重载函数的实现。当你超载时 function statement ,TypeScript检查东西 太松了 。这往往会减少编译器错误,但也可能错误地允许不安全的事情:

    function findOne(carID: number): Promise<t_car_base>
    function findOne(carID: number, joinEntities: true): Promise<t_car_joined>
    function findOne<K extends keyof t_car_joined>(carID: number, fields: K[]): Promise<Pick<t_car_joined, K>>
    
    function findOne<K extends keyof t_car_joined>(
      carID: number,
      joinEntitiesOrFields?: true | K[],
    ) {
      return new Promise<t_car_base | t_car_joined | Pick<t_car_joined, K>>((resolve, reject) => {
        if (!Array.isArray(joinEntitiesOrFields)) { // oops!
          //^ <-- this is bad, but TypeScript didn't complain
          resolve({} as Pick<t_car_joined, K>)
        } else if (joinEntitiesOrFields) {
          resolve({} as t_car_joined)
        } else {
          resolve({} as t_car_base)
        }
      })
    }
    

    对于TypeScript来说,以“正确”的方式检查重载太复杂了,所以它没有。参见 microsoft/TypeScript#13235 .

    另一方面,当你有一个 arrow function 并尝试将其分配给重载签名,TypeScript会进行检查 过于严格 。只有当箭头函数推断的类型适用时,它才允许 每一个 呼叫签名,这很少是真的。在您的示例中,TypeScript对您有时返回感到不安 选择<t_car_joined,K> 当其中一个呼叫签名期望 t_car_base 这检查太严格了。

    所以它不会让你做不安全的事情,但也不会让你去做安全的事情。有一个请求 microsoft/TypeScript#47669 至少使其工作方式类似于函数语句,但就目前而言,如果将箭头函数分配给重载函数类型的变量,则可能会收到至少一个调用签名不匹配的错误消息,即使它们确实匹配。

    目前,如果你想使用重载箭头函数,你必须通过有意放松类型检查来避免这个问题,例如使用 type assertion :

    const findOne = (<K extends keyof t_car_joined>(
      carID: number,
      joinEntitiesOrFields?: true | K[],
    ) => {
      return new Promise<t_car_base | t_car_joined | Pick<t_car_joined, K>>((resolve, reject) => {
        if (Array.isArray(joinEntitiesOrFields)) {
          resolve({} as Pick<t_car_joined, K>)
        } else if (joinEntitiesOrFields) {
          resolve({} as t_car_joined)
        } else {
          resolve({} as t_car_base)
        }
      })
    }) as t_findOne;
    

    这类似于使用函数语句进行重载。是的,可能会出错并返回错误的东西,就像重载函数语句一样。这只是意味着你需要小心并测试你的函数实现。但至少你没有收到错误消息。


    Playground link to code