【问题标题】:TypeScript function return type based on input parameter基于输入参数的 TypeScript 函数返回类型
【发布时间】:2019-06-07 11:57:21
【问题描述】:

我有几个不同的接口和对象,每个都有type 属性。假设这些是存储在 NoSQL 数据库中的对象。如何根据输入参数type 创建具有确定性返回类型的通用getItem 函数?

interface Circle {
    type: "circle";
    radius: number;
}

interface Square {
    type: "square";
    length: number;
}

const shapes: (Circle | Square)[] = [
    { type: "circle", radius: 1 },
    { type: "circle", radius: 2 },
    { type: "square", length: 10 }];

function getItems(type: "circle" | "square") {
    return shapes.filter(s => s.type == type);
    // Think of this as items coming from a database
    // I'd like the return type of this function to be
    // deterministic based on the `type` value provided as a parameter. 
}

const circles = getItems("circle");
for (const circle of circles) {
    console.log(circle.radius);
                       ^^^^^^
}

类型“圆|”上不存在属性“半径”方”。

【问题讨论】:

    标签: typescript


    【解决方案1】:

    您正在寻找overload signatures

    function getItems(type: "circle"): Circle[]
    function getItems(type: "square"): Square[]
    function getItems(type: "circle" | "square") {
        return shapes.filter(s => s.type == type);
    }
    

    在实际定义之前放置多个类型签名允许您列出函数签名可能落入的不同“情况”。

    发表评论后编辑

    所以事实证明,你想要的是可能,但我们可能需要跳过几个圈才能到达那里。

    首先,我们需要一种方法来翻译每个名称。我们希望"circle" 映射到Circle"square"Square 等等。为此,我们可以使用conditional type

    type ObjectType<T> =
      T extends "circle" ? Circle :
      T extends "square" ? Square :
      never;
    

    (我使用never 作为后备,希望如果您以某种方式最终得到无效类型,它会很快产生类型错误)

    现在,我不知道有一种方法可以像您要​​求的那样对函数调用的类型进行参数化,但是 Typescript 确实支持通过以下方式对对象的键进行参数化mapped typed。因此,如果您愿意将getItems("circle") 语法换成getItems["circle"],我们至少可以描述类型。

    interface Keys {
      circle: "circle";
      square: "square";
    }
    
    type GetItemsType = {
      [K in keyof Keys]: ObjectType<K>[];
    }
    

    问题是,我们现在必须真正构造一个这种类型的对象。如果你的目标是 ES2015(编译时--target es2015 或更新版本),你可以使用 Javascript Proxy 类型。现在,不幸的是,我不知道有什么好方法可以让 Typescript 相信我们正在做的事情是好的,所以通过any 快速转换将平息它的担忧。

    let getItems: GetItemsType = <any>new Proxy({}, {
      get: function(target, type) {
        return shapes.filter(s => s.type == type);
      }
    });
    

    所以你失去了对实际getItems“函数”的类型检查,但你在调用站点获得了更强的类型检查。然后,拨打电话,

    const circles = getItems["circle"];
    for (const circle of circles) {
        console.log(circle.radius);
    }
    

    这值得吗?这取决于你。这是很多额外的语法,你的用户必须使用[] 表示法,但它会得到你想要的结果。

    【讨论】:

    • 对,我就是这样开始的,但是类型的数量增加了,然后函数的数量增加了(get、put、update、upsert、delete、query 等),所以重载签名变得巨大。是否有其他可用的 TypeScript 功能可以做到这一点?例如,我可以定义interface ObjectTypes { circle: Circle, square: Square }... 有什么聪明的方法可以基本上做到function get(type: "square" | "circle"): ObjectTypes[type] 吗?
    • 谢谢西尔维奥。我受到您的想法的启发并使用了Conditional Type 想法。在下面发布我的最终解决方案。你怎么看?
    • 对于其他更简单的情况,这个答案中的函数重载方法可能是更容易阅读的解决方案。
    【解决方案2】:
    As you have mentioned that data is comming from No-Sql database with a type property. you can create type property as string value and change your interfaces as a class to check instanceOf in your function.
    
    class Circle {
        type: string;
        radius: number;
    }
    
    class Square {
        type: string;
        length: number;
    }
    
    const shapes: (Circle | Square)[] = [
        { type: "circle", radius: 1 },
        { type: "circle", radius: 2 },
        { type: "square", length: 10 }];
    
    function getItems(type: string) {
        return shapes.filter(s => s.type == type);
        // Think of this as items coming from a database
        // I'd like the return type of this function to be
        // deterministic based on the `type` value provided as a parameter. 
    }
    
    const circles = getItems("circle");
    for (const circle of circles) {
        if (circle instanceof Circle) {
            console.log(circle.radius);
        } else if (circle instanceof Square) {
            console.log(circle.length);
        }
    } 
    

    【讨论】:

    • 这有什么帮助?
    【解决方案3】:

    Conditional Types 救援:

    interface Circle {
        type: "circle";
        radius: number;
    }
    
    interface Square {
        type: "square";
        length: number;
    }
    
    type TypeName = "circle" | "square"; 
    
    type ObjectType<T> = 
        T extends "circle" ? Circle :
        T extends "square" ? Square :
        never;
    
    const shapes: (Circle | Square)[] = [
        { type: "circle", radius: 1 },
        { type: "circle", radius: 2 },
        { type: "square", length: 10 }];
    
    function getItems<T extends TypeName>(type: T) : ObjectType<T>[]  {
        return shapes.filter(s => s.type == type) as ObjectType<T>[];
    }
    
    const circles = getItems("circle");
    for (const circle of circles) {
        console.log(circle.radius);
    }
    

    感谢 Silvio 为我指明了正确的方向。

    【讨论】:

    • 哦,这比我建议的黑客要简单得多。我把它归咎于我现在所在的午夜,但是是的,这是一个更好的解决方案。干得好,很高兴能为您提供帮助!
    • @ArashMotamedi 我认为您必须接受自己的答案,因为这正是您所要求的。请注意,TypeName 可以是枚举。
    • @ArashMotamedi 可能是这个问题的扩展,我如何修改这个函数,以便我可以访问 s.radius 或 s.length 而不是 s.type,具体取决于传入的函数类型``` getItems&lt;T extends TypeName&gt;(type: T) : ObjectType&lt;T&gt;[] { return shapes.filter(s =&gt; s.length == 1) as ObjectType&lt;T&gt;[]; } 或 ` ``` getItems&lt;T extends TypeName&gt;(type: T) : ObjectType&lt;T&gt;[] { return shapes.filter(s =&gt; s.radius == 1) as ObjectType&lt;T&gt;[]; }Typescript 抛出一个错误,指出 Circle 类型不存在长度。我想使用相同的函数但访问差异变量。
    • 太棒了。非常感谢!
    • 你能修改这个,让type参数是可选的吗? type?: Ttype: T = "circle" 目前都不能使用它。
    猜你喜欢
    • 2019-12-19
    • 1970-01-01
    • 2023-03-30
    • 1970-01-01
    • 2017-06-01
    • 1970-01-01
    • 1970-01-01
    • 2022-11-04
    • 1970-01-01
    相关资源
    最近更新 更多