带有布尔检查的 TypeScript 上的条件返回类型似乎不起作用

Conditional return type on TypeScript with boolean check doesn't seem to work

提问人:Mosti 提问时间:11/2/2023 最后编辑:T.J. CrowderMosti 更新时间:11/2/2023 访问量:68

问:

给定以下代码:

type OpenFileDialogReturn<TMulti extends boolean> =
  TMulti extends true ? FileList | null | undefined :
  TMulti extends false ? File | null | undefined :
  never

type OpenFileDialog = {
  (options: { multiple?: false, acceptedFileTypes?: string }): Promise<OpenFileDialogReturn<false>>
  (options: { multiple: true, acceptedFileTypes?: string }): Promise<OpenFileDialogReturn<true>>
}

export const openFileDialog: OpenFileDialog = (options = { }) => {
  return new Promise(resolve => {
    const input = document.createElement('input')
    input.type = 'file'
    input.style.opacity = '0'

    if (options.acceptedFileTypes) input.accept = options.acceptedFileTypes
    if (options.multiple) input.multiple = true

    input.addEventListener('input', () => {
      if (options && options.multiple === true) resolve(input.files)

      const file = input.files?.[0]
      if (!file) resolve(null)

      resolve(file)
    })

    input.click()
  })
}

TypeScript 在最后一个抛出错误resolve(file)

Argument of type 'File | undefined' is not assignable to parameter of type 'FileList | PromiseLike<FileList | null | undefined> | null | undefined'.
  Type 'File' is not assignable to type 'FileList | PromiseLike<FileList | null | undefined> | null | undefined'.ts(2345)
const file: File | undefined

我尝试了几种方法,甚至将所有内容包装在严格的 if/else 块中,以确保 TS 理解此时是严格的,但似乎没有任何效果。任何帮助都是值得赞赏的。options.multiplefalse

游乐场链接

JavaScript 打字稿

评论

0赞 Felix Kling 11/2/2023
resolve(file)即使 是 ,也会执行。你需要一个声明。但实际问题似乎是似乎并不期望收到.if (file)trueelseresolveFile
0赞 Mosti 11/2/2023
嘿,@FelixKling当文件不是未定义时执行的,并且类型似乎正确键入到 ,但它仍然抱怨它不是奇怪的,因为它知道此时是错误的resolve(file)FileFileListoptions.multiple
1赞 Mosti 11/2/2023
@T.J.Crowder感谢您的回答,这正是我一直在寻找的。主要是内部承诺采用第一种类型的事实是完全有道理的。您想将您的答案复制到实际答案以便我可以标记它吗?
0赞 T.J. Crowder 11/2/2023
@Mosti - 完成。我认为我不理解的事情有细微差别,但这似乎就是你的代码中发生的事情。(不过,我还不能单独向自己证明这一点。Promise
0赞 Felix Kling 11/3/2023
“resolve(file) 在文件未未定义时执行”对不起,我的意思是即使被执行也会被执行。多次调用没有任何效果。此外,如果承诺可以解决,无论如何,为什么你甚至有声明?resolve(file)if(!file) resolve(null)resolvenullundefinedif (!file) resolve(null)

答:

0赞 T.J. Crowder 11/2/2023 #1

由于您没有在函数中创建的 类型参数,TypeScript 似乎正在选择您的重载之一(那个),因此它拒绝了使用 .(我不确定那里的分辨率的细节,但它似乎与重载的顺序有关;我不会依赖它。然后,由于其返回类型,实现函数与该类型不兼容。PromiseFileList | null | undefinedresolveFileOpenFileDialog

比我更好的 TypeScript 负责人告诉我,重载函数的实现与其所有重载签名不严格兼容并不罕见——也就是说,实现不是完全类型安全的。在这种情况下,您可以键入 promise as,以便至少 promise 执行器中的代码被限制为返回其中之一(即使从技术上讲,编码错误可能会让它在应该返回 a 时返回 a ),但随后对结果 (, ugh) 使用类型断言使实现可分配给 。(或者,您可以在一开始就使用 any 作为类型参数,但随后执行器中的代码是完全不受约束的。File | FileList | null | undefinedFileListFileas Promise<any>OpenFileDialog

您还需要进行一个不相关的小更改:在 中的签名中标记为可选,因为您的实现为其提供了默认值。optionsfalseOpenFileDialog

像这样的东西:

type OpenFileDialogReturn<TMulti extends boolean> =
  TMulti extends true ? FileList | null | undefined :
  TMulti extends false ? File | null | undefined :
  never

type OpenFileDialog = {
  (options?: { multiple?: false, acceptedFileTypes?: string }): Promise<OpenFileDialogReturn<false>>
//        ^−−−−−−−−−−− make `options` optional in this signature
  (options: { multiple: true, acceptedFileTypes?: string }): Promise<OpenFileDialogReturn<true>>
}

export const openFileDialog: OpenFileDialog = (options = { }) => {
  return new Promise<File | FileList | null | undefined>(resolve => {
//                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    const input = document.createElement('input')
    input.type = 'file'
    input.style.opacity = '0'

    if (options.acceptedFileTypes) input.accept = options.acceptedFileTypes
    if (options.multiple) input.multiple = true

    input.addEventListener('input', () => {
      if (options && options.multiple === true) resolve(input.files)

      const file = input.files?.[0]
      if (!file) resolve(null)

      resolve(file)
    })

    input.click()
  }) as Promise<any>;
//   ^^^^^^^^^^^^^^^−−−−− to make the implementation compatible with the overloads (ugh)
}

检查使用情况:

const x = await openFileDialog({multiple: true});
//    ^? −−− const x: FileList | null | undefined
const y = await openFileDialog();
//    ^? −−− const y: File | null | undefined
const z = await openFileDialog({multiple: false});
//    ^? −−− const z: File | null | undefined

游乐场链接