在 TypeScript 中重载联合类型

Posted

技术标签:

【中文标题】在 TypeScript 中重载联合类型【英文标题】:Overloading union types in TypeScript 【发布时间】:2021-04-11 12:27:27 【问题描述】:

我想定义一个函数f,使得参数和返回类型是联合类型,但它们是绑定的,当您调用f(anInputType) 时,您会得到aCorrespondingOutputType。我尝试了以下方法,它似乎适用于 TypeScript 4.1.3。

class Input1 
    type1Input: string = "I am type 1!";


class Input2 
    type2Input: string = "I am type 2!";


interface Output1 
    type1Output: string;


interface Output2 
    type2Output: string;


export type Input = Input1 | Input2;
export type Output = Output1 | Output2;

function f(_input: Input1): Output1;
function f(_input: Input2): Output2;
function f(_input: Input): Output 
    return null as any as Output;


const input = new Input2();

const output = f(input);
output.type2Output // Compiles because the compiler can figure out that output is of type Output2

但是,在(我的)现实生活中,输入应该是 WebGL 对象。同样的事情,我将Input1 替换为WebGLTextureInput2 替换为WebGLBuffer,无法编译。

interface Output1 
    type1Output: string;


interface Output2 
    type2Output: string;


export type Output = Output1 | Output2;

function f(_input: WebGLTexture): Output1;
function f(_input: WebGLBuffer): Output2;
function f(_input: WebGLTexture | WebGLBuffer): Output 
    return null as any as Output;


const canvas = document.createElement("canvas")!;
const gl = canvas.getContext("webgl")!;
const input = gl.createBuffer()!;

const output = f(input);
output.type2Output // Does not compile because the compiler thinks that output is of type Output1

我错过了什么吗?

我正在阅读这些 TypeScript 问题:

https://github.com/Microsoft/TypeScript/issues/27131 https://github.com/microsoft/TypeScript/issues/14107

以及这些其他问题:

Typescript overloading functions with union types Typescript function overloads not working for the union type case

但令我困扰的是使用现有类型( WebGL 对象)与自定义类的行为之间的差异。

其实我是采用Alex的想法

它看起来很冗长,但有了它我可以摆脱一个单一的实现。由于应用程序其他部分的组织方式,这对我来说有点失败。 某事,某处,会看起来很丑。但这现在很好!谢谢!

下面的真实代码。

export type WebGLResource =
   texture: WebGLTexture  |
   buffer: WebGLBuffer  |
   program: WebGLProgram  |
   renderbuffer: WebGLRenderbuffer  |
   framebuffer: WebGLFramebuffer ;
export type ResourceMeta = TextureMeta | BufferMeta | ProgramMeta | RenderbufferMeta | FramebufferMeta;

function getMeta(<...omitted params...> resource:  texture: WebGLTexture , required: true): TextureMeta;
function getMeta(<...omitted params...> resource:  buffer: WebGLBuffer , required: true): BufferMeta;
function getMeta(<...omitted params...> resource:  program: WebGLProgram , required: true): ProgramMeta;
function getMeta(<...omitted params...> resource:  renderbuffer: WebGLRenderbuffer , required: true): RenderbufferMeta;
function getMeta(<...omitted params...> resource:  framebuffer: WebGLFramebuffer , required: true): FramebufferMeta;
function getMeta(<...omitted params...> resource:  texture: WebGLTexture , required: false): TextureMeta | null;
function getMeta(<...omitted params...> resource:  texture: WebGLBuffer , required: false): BufferMeta | null;
function getMeta(<...omitted params...> resource:  buffer: WebGLProgram , required: false): ProgramMeta | null;
function getMeta(<...omitted params...> resource:  renderbuffer: WebGLRenderbuffer , required: false): RenderbufferMeta | null;
function getMeta(<...omitted params...> resource:  framebuffer: WebGLFramebuffer , required: false): FramebufferMeta | null;
function getMeta(<...omitted params...> resource: WebGLResource, required: boolean): ResourceMeta | null 
  ...

【问题讨论】:

【参考方案1】:

Typescript 是结构性的,而不是名义上的。这意味着如果两种类型具有相同的形状,但名称不同,则它们被认为是相同的类型。不幸的是,WebGLTextureWebGLBuffer 的形状似乎相同。

如果您在 VSCode 或 typescript playground 中 cmd+click(或 ctrl+click)这些类型中的任何一种,您可以看到它们是如何声明的。这产生了这两个声明:

interface WebGLTexture extends WebGLObject 
interface WebGLBuffer extends WebGLObject 

遗憾的是,这两种类型都被简单地声明为未更改的WebGLObjects。 Typescript 无法区分它们。因此,当您尝试触发第二个函数重载时,typescript 会注意到第一个重载匹配,并使用它。

很难从这个人为的代码中找到更好的解决方案,但您可能会想要一种不同的方法。


也许您可以改为接受一个带有键的对象,以明确您传入的内容?这样不同的重载并有不同的参数类型。

function f(_input:  texture: WebGLTexture ): Output1;
function f(_input:  buffer: WebGLBuffer ): Output2;
function f(_input:  texture?: WebGLTexture, buffer?: WebGLBuffer ): Output 
    return null as any as Output;


const canvas = document.createElement("canvas")!;
const gl = canvas.getContext("webgl")!;

const buffer = gl.createBuffer()!;
const texture = gl.createTexture()!;

f( texture ).type1Output; // works
f( buffer ).type2Output; // works

Playground

【讨论】:

谢谢你,亚历克斯,很棒的答案。我正在研究的一大卖点是它几乎在所有地方都使用原生 WebGL 类型。我想在这里做的是,在很多方面,包装的替代品。尽管我不得不说,由于缩短了 property 语法,您的代码看起来非常好。有一天我肯定会用这个。对于这个项目,对我来说最好的选择可能是放弃重载并提供单独的函数(我只有 5 种类型要映射;纹理、缓冲区、帧缓冲区、渲染缓冲区和程序)。谢谢! 是的,重载可能有点痛苦。如果使用得当,它们可以强大且富有表现力,但从 fBuffer(buffer)fTexture(texture) 开始也没有错。不那么棘手,更容易理解,代码通常是好的。乐意效劳。祝你好运! 这个问题的解决方法似乎很晦涩。为什么有人会这样做? > 这个问题的解决方法似乎很晦涩。是的,这不是很好,但我的问题是我有这个返回 WebGL 资源的类;我希望它们作为原始 WebGL 对象返回,而不是代理或包装或不透明或类似的东西。用户可以随时将这些对象传回,我需要访问一些每个对象的附加信息来处理这些请求。我暂时不得不忍受这个,但我今天学到的最有可能的结果是,我可能会重构代码以不返回原始 WebGL 对象。 拥有多个函数实现的优势在于,使用重载我只需要编写一次主体。

以上是关于在 TypeScript 中重载联合类型的主要内容,如果未能解决你的问题,请参考以下文章

如何区分 TypeScript 重载中的类型对象和数组?

TypeScript系列教程16TypeScript 联合类型

TypeScript(14): 联合类型

Typescript泛型函数重载

[TypeScript] type、typeof、keyof

在 onClick 道具中使用联合类型