使用TypeScript构建一个具有属性的函数对象

142

我想创建一个函数对象,其中还保存着一些属性。例如在JavaScript中,我会这样做:

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

现在在TypeScript中,我可以描述this的类型为:

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

然而,如果不进行强制类型转换,我无法构建它。例如:

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

你如何在不使用强制类型转换的情况下构建这个?


2
这不是对你问题的直接回答,但是对于任何想要简洁地构建具有属性的函数对象,并且可以进行强制转换的人来说,Object-Spread operator似乎可以解决问题:var f: { (): any; someValue: number; } = <{ (): any; someValue: number; }>{ ...(() => "Hello"), someValue: 3 };. - Jonathan
9个回答

137
现在(typescript 2.x)使用 Object.assign(target, source) 可以轻松实现此功能。
示例:

enter image description here

神奇的是,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
错误:number 无法赋值给 string[](返回类型)

注意:如果针对旧浏览器,则可能需要polyfill Object.assign。


4
太棒了,谢谢。截至2017年2月,这是最好的答案(适用于Typescript 2.x用户)。 - Andrew Faulkner
在2021年,这仍然似乎是最好的答案。 - Xunnamius
1
为了告诉 TypeScript 类型是什么,而改变实际可运行的代码感觉很不好。 - Oleg Mihailik
很遗憾,似乎不能与泛型一起使用。 - undefined

126

更新:在 TypeScript 的早期版本中,这个答案是最好的解决方案,但在较新的版本中有更好的选择(请参见其他答案)。

虽然被接受的答案可行并且在某些情况下可能是必需的,但其缺点是无法提供构建对象时的类型安全性。使用这种技术,如果您尝试添加未定义的属性,它会至少抛出一个类型错误。

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

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

// type error
f.notDeclard = 3

3
我不明白为什么 var f 这一行不会导致错误,因为在那时并没有 someValue 属性。 - user663031
4
由于它不检查强制类型转换,因此出现了这种情况。要进行类型检查,需要执行以下操作:var f:F = function(){},但在上面的示例中,这将失败。在更复杂的情况下,该答案并不十分适用,因为您会在赋值阶段失去类型检查。 - Meirion Hughes
1
正确,但如何使用函数声明而不是函数表达式来实现这个? - Alexander Mills
2
在最新版本的TS中,这种方式将不再起作用,因为接口检查更加严格,函数的强制转换由于缺少所需的“someValue”属性而无法编译。但是,可以使用<F><any>function() { ... } - galenus
让我成功的关键是接口中的 `():any' 必须存在,且不能有返回值。因此,即使函数返回数字/字符串/类,也不能指定它们。 - Drenai
显示剩余2条评论

64

TypeScript旨在通过声明合并来处理此类情况:

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

声明合并使我们可以同时将某些内容声明为函数和命名空间(内部模块):

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

这样做保留了类型,并且使我们能够编写f()f.someValue。在为现有JavaScript代码编写.d.ts文件时,请使用declare

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

在 TypeScript 中,给函数添加属性通常会让人感到困惑或意料之外,因此请尽量避免使用,但在使用或转换旧的 JS 代码时可能是必要的。这是混合使用内部模块(命名空间)和外部模块的情况下唯一适用的情况之一。


在答案中提到环境模块就点个赞吧。这是在转换或标注现有的 JS 模块时非常典型的情况。正是我所需要的! - Nipheris
1
很好。如果你想在ES6模块中使用它,你可以这样做:function hello() { .. } export const value = 5; }``` ```export default hello;``` 我认为这比Object.assign或类似的运行时hack要干净得多。没有运行时,没有类型断言,什么都没有。 - skrebbel
1
这个答案很好,但是如何将Symbol.iterator这样的符号附加到一个函数上呢? - kzh
上面的链接已经失效了,正确的链接应该是https://www.typescriptlang.org/docs/handbook/declaration-merging.html。 - iJungleBoy
你应该知道,声明合并会导致一些相当奇怪的 JavaScript 代码。它会添加一个额外的闭包,对于简单的属性赋值来说似乎有点过度了。这不是 JavaScript 的常规做法。它还会引入依赖顺序问题,导致结果值不一定是一个函数,除非你确定该函数是第一个被“require”的东西。 - John Leidegren
叮叮叮!这就是正确的方法:不需要断言,也不需要像其他大多数答案那样将函数声明分成两部分。 - fregante

33

因此,如果要求是仅构建并将该函数分配给"f"而无需转换,这里有一个可能的解决方案:

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

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

本质上,它使用自执行函数字面量来“构建”一个将在赋值之前匹配该签名的对象。唯一奇怪的是,函数的内部声明需要是“任意类型”,否则编译器会抱怨你正在给尚不存在于对象上的属性赋值。

编辑:代码有所简化。


7
据我所理解,这实际上不会检查类型,因此 .someValue 可能基本上是任何东西。 - shabunc
3
以下是2017/2018年TypeScript 2.0的更好/更新答案,由Meirion Hughes在以下链接中发布:https://dev59.com/-mcs5IYBdhLWcg3wiEiK。 - user3773048

16

虽然这是一个老问题,但对于 TypeScript 3.1 及以上版本,只要使用函数声明或 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'"

参考资料在线示例


3

我不能说这很简单,但绝对是可行的:

interface Optional {
  <T>(value?: T): OptionalMonad<T>;
  empty(): OptionalMonad<any>;
}

const Optional = (<T>(value?: T) => OptionalCreator(value)) as Optional;
Optional.empty = () => OptionalCreator();

如果你感到好奇,这是我一个代码片段的TypeScript/JavaScript版本的Optional


这应该是最好的答案,它既有类型又有实现。 - Acid Coder
如何在第一个成员中输入整个方法,我的意思是,如果我只是不能使用 (): void; 代替类型,例如:express.RequestHandler: void; 或类似的东西作为第一个成员,这样我就不用每个函数参数都加上它的类型了... - Máxima Alekz

3
作为一个快捷方式,你可以使用 ['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

然而,如果你真的想要对属性值进行类型检查,这样做是行不通的。


这还能用吗?我在我的.ts文件中无法让它工作。 - Riza Khan
不要这样做,这很老旧。 - Rick Love

2

一份更新后的答案:自从通过&添加了交叉类型,现在可以“合并”两个推断出的类型。

下面是一个通用的帮助函数,它读取某个对象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类型。如果将bar重命名为bad,则会出现类型错误。

NB但是,这里仍然存在一种类型漏洞。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;
}

0

这种做法背离了强类型的规范,但你是可以这样做的

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

如果你像我一样试图绕过强类型限制,那么你可能会遇到这个问题。不幸的是,TypeScript在处理完全有效的JavaScript时会失败,因此你必须告诉TypeScript退后一步。

"你的JavaScript代码是完全有效的TypeScript"这句话的结果是false。(注意:使用0.95版本)


这个解答虽然不是最好的,但是它救了我。 - Riza Khan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接