使用TypeScript中的属性构建函数对象


83

我想创建一个函数对象,该对象还具有一些属性。例如,在JavaScript中,我会这样做:

var f = function() { }
f.someValue = 3;

现在,在TypeScript中,我可以将其描述为:

var f: { (): any; someValue: number; };

但是,如果没有强制转换,我实际上无法构建它。如:

var f: { (): any; someValue: number; } =
    <{ (): any; someValue: number; }>(
        function() { }
    );
f.someValue = 3;

没有铸造,您将如何构建它?


2
这不是您问题的直接答案,但是对于任何想要简洁地使用属性构建函数对象并且可以转换的人来说,Object-Spread运算符似乎可以解决问题:var f: { (): any; someValue: number; } = <{ (): any; someValue: number; }>{ ...(() => "Hello"), someValue: 3 };
乔纳森

Answers:


31

因此,如果只需要简单地构建该函数并将其分配给“ f”而不进行强制转换,则可以采用以下解决方案:

var f: { (): any; someValue: number; };

f = (() => {
    var _f : any = function () { };
    _f.someValue = 3;
    return _f;
})();

本质上,它使用自动执行函数文字来“构造”一个​​对象,该对象将在分配完成之前与该签名匹配。唯一奇怪的是,函数的内部声明必须为“ any”类型,否则编译器会要求您将其分配给对象上尚不存在的属性。

编辑:简化了一点代码。


5
据我了解,这实际上不会检查类型,因此.someValue本质上可以是任何东西。
shabunc 2015年

1
以下是Meirion Hughes在2017/2018年发布的针对TypeScript 2.0的更好/更新的答案:stackoverflow.com/questions/12766528/…
user3773048

107

更新:此答案是TypeScript早期版本中的最佳解决方案,但是较新版本中提供了更好的选项(请参阅其他答案)。

可接受的答案是可行的,并且在某些情况下可能是必需的,但是不利的一面是无法为构建对象提供类型安全性。如果您尝试添加未定义的属性,则该技术至少会引发类型错误。

interface F { (): any; someValue: number; }

var f = <F>function () { }
f.someValue = 3

// type error
f.notDeclard = 3

1
我不明白为什么该var f行不会引起错误,因为那时没有someValue属性。

2
@torazaburo,因为它在您投射时不检查。要键入检查,您需要执行以下操作:var f:F = function(){}在上面的示例中这将失败。对于更复杂的情况,此答案不是很好,因为您在分配阶段丢失了类型检查。
Meirion Hughes

1
是的,但是如何使用函数声明而不是函数表达式来实现呢?
亚历山大·米尔斯

1
由于接口检查更加严格,并且由于缺少必需的“ someValue”属性,因此函数的强制转换将不再编译,因此在TS的最新版本中将不再起作用。不过,有效的方法是<F><any>function() { ... }
galenus

当您使用tslint:recommended时,类型转换不起作用,您需要使用的是:(将函数包装在方括号中以使其成为表达式;然后将其用作F):var f = (function () {}) as any as F;
NikhilWanpal

78

现在(使用脚本2.x)可以轻松实现 Object.assign(target, source)

例:

在此处输入图片说明

这里的魔力Object.assign<T, U>(t: T, u: U)是键入返回交集 T & U

强制将其解析为已知接口也很简单。例如:

interface Foo {
  (a: number, b: string): string[];
  foo: string;
}

let method: Foo = Object.assign(
  (a: number, b: string) => { return a * a; },
  { foo: 10 }
); 

哪些错误归因于不兼容的输入:

错误:无法将foo:number分配给foo:string
错误:无法将数字分配给string [](返回类型)

注意:如果定位到较旧的浏览器,则可能需要填充Object.assign。


1
太好了,谢谢。截至2017年2月,这是最佳答案(对于Typescript 2.x用户)。
安德鲁·福克纳

现在是2020年,有没有更新的解决方案?
Archsx

46

TypeScript旨在通过声明合并处理这种情况:

您可能还熟悉JavaScript的做法,即创建一个函数,然后通过向该函数添加属性来进一步扩展该函数。TypeScript使用声明合并以类型安全的方式建立这样的定义。

声明合并使我们说某些东西既是函数又是名称空间(内部模块):

function f() { }
namespace f {
    export var someValue = 3;
}

这样可以保留输入内容,并让我们同时编写f()f.someValue.d.ts为现有的JavaScript代码编写文件时,请使用declare

declare function f(): void;
declare namespace f {
    export var someValue: number;
}

在TypeScript中,向函数添加属性通常是令人困惑或意外的模式,因此请尽量避免使用它,但是在使用或转换较旧的JS代码时,有必要这样做。这是将内部模块(命名空间)与外部模块混合使用的唯一机会之一。


支持在答案中提及环境模块。在转换或注释现有JS模块时,这是非常典型的情况。正是我想要的!
Nipheris

可爱。如果要将其与ES6模块一起使用,则只需function hello() { .. } namespace hello { export const value = 5; } export default hello;IMO,这比Object.assign或类似的运行时黑客要干净得多。没有运行时,没有类型断言,什么都没有。
skrebbel

这个答案很好,但是如何将Symbol.iterator之类的Symbol附加到函数上呢?
kzh

上面的链接不工作了,正确的是typescriptlang.org/docs/handbook/declaration-merging.html
iJungleBoy

您应该知道,声明合并会导致一些非常奇怪的JavaScript。它增加了一个额外的闭包,对我来说,简单的属性分配似乎有些多余。这是非常非JavaScript的方式。它还引入了依赖性问题的顺序,其中产生的价值不一定是功能,除非你确信的功能是可以在第一件事情需要
约翰·莱德格伦


2

作为快捷方式,您可以使用['property']访问器动态分配对象值:

var f = function() { }
f['someValue'] = 3;

这绕过了类型检查。但是,这非常安全,因为您必须以相同的方式有意访问该属性:

var val = f.someValue; // This won't work
var val = f['someValue']; // Yeah, I meant to do that

但是,如果您确实希望对属性值进行类型检查,则此方法将无效。


1

更新的答案:由于通过添加了交叉路口类型&,因此可以即时“合并”两个推断的类型。

这是一个通用帮助程序,它读取某些对象的属性from并将其复制到对象上onto。它返回相同的对象,onto但具有包含两组属性的新类型,因此可以正确描述运行时行为:

function merge<T1, T2>(onto: T1, from: T2): T1 & T2 {
    Object.keys(from).forEach(key => onto[key] = from[key]);
    return onto as T1 & T2;
}

该低级帮助程序仍会执行类型声明,但是从设计上讲它是类型安全的。有了此辅助程序,我们就可以使用操作员来解决OP的全类型安全问题:

interface Foo {
    (message: string): void;
    bar(count: number): void;
}

const foo: Foo = merge(
    (message: string) => console.log(`message is ${message}`), {
        bar(count: number) {
            console.log(`bar was passed ${count}`)
        }
    }
);

单击此处在TypeScript Playground中尝试一下。请注意,我们已限制foo为类型Foo,因此结果merge必须是完整的Foo。因此,如果您重命名barbad则会出现类型错误。

注意:这里仍然有一个类型的孔。TypeScript没有提供将类型参数约束为“不是函数”的方法。因此,您可能会感到困惑,并将函数作为第二个参数传递给merge,这将不起作用。因此,在可以声明之前,我们必须在运行时捕获它:

function merge<T1, T2>(onto: T1, from: T2): T1 & T2 {
    if (typeof from !== "object" || from instanceof Array) {
        throw new Error("merge: 'from' must be an ordinary object");
    }
    Object.keys(from).forEach(key => onto[key] = from[key]);
    return onto as T1 & T2;
}

1

这是一个老问题,但是对于从3.1开始的TypeScript版本,只要使用函数声明或const变量关键字,就可以像在普通JS中那样简单地进行属性分配:

function f () {}
f.someValue = 3; // fine
const g = function () {};
g.someValue = 3; // also fine
var h = function () {};
h.someValue = 3; // Error: "Property 'someValue' does not exist on type '() => void'"

参考在线示例


0

这与强力打字不同,但是您可以

var f: any = function() { }
f.someValue = 3;

如果您想避开像我发现这个问题时那样的压迫性强打字。不幸的是,这是TypeScript在完全有效的JavaScript上失败的情况,因此您必须告诉TypeScript退出。

“您的JavaScript是完全有效的TypeScript”评估为false。(注意:使用0.95)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.