138

I want to create a function object, which also has some properties held on it. For example in JavaScript I would do:

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

Now in TypeScript I can describe the type of this as:

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

However I can't actually build it, without requiring a cast. Such as:

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

How would you build this without a cast?

Jonathan
  • 32,202
  • 38
  • 137
  • 208
JL235
  • 2,455
  • 2
  • 19
  • 14
  • 2
    This isn't a direct answer to your question, but for anyone who wants to concisely build a function object with properties, and is OK with casting, the [Object-Spread operator](https://blog.mariusschulz.com/2016/12/23/typescript-2-1-object-rest-and-spread) seems to do the trick: `var f: { (): any; someValue: number; } = <{ (): any; someValue: number; }>{ ...(() => "Hello"), someValue: 3 };`. – Jonathan Nov 29 '17 at 19:21

9 Answers9

130

This is easily achievable now (typescript 2.x) with Object.assign(target, source)

example:

enter image description here

The magic here is that Object.assign<T, U>(t: T, u: U) is typed to return the intersection T & U.

Enforcing that this resolves to a known interface is also straight-forward. For example:

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

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

which errors due to incompatible typing:

Error: foo:number not assignable to foo:string
Error: number not assignable to string[] (return type)

caveat: you may need to polyfill Object.assign if targeting older browsers.

Meirion Hughes
  • 24,994
  • 12
  • 71
  • 122
125

Update: This answer was the best solution in earlier versions of TypeScript, but there are better options available in newer versions (see other answers).

The accepted answer works and might be required in some situations, but have the downside of providing no type safety for building up the object. This technique will at least throw a type error if you attempt to add an undefined property.

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

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

// type error
f.notDeclard = 3
Greg Weber
  • 3,228
  • 4
  • 21
  • 22
  • 3
    I don't understand why the `var f` line does not cause an error, since at that time there is no `someValue` property. –  Jul 14 '16 at 16:39
  • 4
    @torazaburo its because it does not check when you cast. To type check you need to do: `var f:F = function(){}` which will fail in the above example. This answer isn't great for more complicated situations as you lose type checking at the assignment stage. – Meirion Hughes Jan 26 '17 at 09:16
  • 1
    right but how do you do this with a function declaration instead of a function expression? – Alexander Mills Apr 02 '17 at 20:27
  • 2
    This will no longer work in the recent versions of TS, because the interface check is much stricter and the casting of function will no longer compile due to absence of required 'someValue' property. What will work, though, is `function() { ... }` – galenus Jul 27 '17 at 07:25
  • when you use tslint:recommended, typecast don't work and what you need to use is: (wrap the function in brackets to make it an expression; then use as any as F): ```var f = (function () {}) as any as F;``` – NikhilWanpal Nov 14 '17 at 12:42
  • The key to getting this to work for me was that `():any' in interface must be present, and cannot have a return value. So even if the function returns a number/string/Class, you cannot specify this – Drenai Dec 21 '17 at 09:27
  • This is the right solution, too bad adding `` makes it an expression. – jcubic Aug 04 '23 at 19:34
63

TypeScript is designed to handle this case through declaration merging:

you may also be familiar with JavaScript practice of creating a function and then extending the function further by adding properties onto the function. TypeScript uses declaration merging to build up definitions like this in a type-safe way.

Declaration merging lets us say that something is both a function and a namespace (internal module):

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

This preserves typing and lets us write both f() and f.someValue. When writing a .d.ts file for existing JavaScript code, use declare:

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

Adding properties to functions is often a confusing or unexpected pattern in TypeScript, so try to avoid it, but it can be necessary when using or converting older JS code. This is one of the only times it would be appropriate to mix internal modules (namespaces) with external.

mk.
  • 11,360
  • 6
  • 40
  • 54
  • Upvote for mentioning ambient modules in the answer. This is very typical case when converting or annotating existing JS modules. Exactly what I am looking for! – Nipheris May 16 '16 at 19:01
  • Lovely. If you want to use this with ES6 modules, you can just do ```function hello() { .. }``` ```namespace hello { export const value = 5; }``` ```export default hello;``` IMO this is much cleaner than Object.assign or similar runtime hacks. No runtime, no type assertions, no nothing. – skrebbel Jul 26 '17 at 19:26
  • 1
    This answer is great, but how do you attach a Symbol such as Symbol.iterator to a function? – kzh Jul 28 '17 at 04:20
  • Link above doesn't work any more, correct would be https://www.typescriptlang.org/docs/handbook/declaration-merging.html – iJungleBoy Sep 05 '17 at 20:56
  • You should know that declaration merging results in some quite wonky JavaScript. It's adds an additional closure which seem excessive to me for a simple property assignment. It's very un-JavaScript way of doing it. It also introduces an order of dependencies problem where the resulting value isn't necessarily a function unless you are certain that the function is the first thing to be _required_. – John Leidegren Aug 27 '18 at 18:55
  • Ding ding ding! This is the right way: no need for assertions nor having to split the function declaration in 2 parts like most other answers. – fregante Nov 22 '20 at 08:16
33

So if the requirement is to simply build and assign that function to "f" without a cast, here is a possible solution:

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

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

Essentially, it uses a self executing function literal to "construct" an object that will match that signature before the assignment is done. The only weirdness is that the inner declaration of the function needs to be of type 'any', otherwise the compiler cries that you're assigning to a property which does not exist on the object yet.

EDIT: Simplified the code a bit.

nxn
  • 3,975
  • 2
  • 20
  • 17
  • 7
    As far as I can understand this won't actually check type, so .someValue could be essentially anything. – shabunc Jan 26 '15 at 00:43
  • 3
    The better / updated answer as of 2017/2018 for TypeScript 2.0 is posted by Meirion Hughes below: https://stackoverflow.com/questions/12766528/build-a-function-object-with-properties-in-typescript#41853194 . – user3773048 Jun 07 '18 at 21:19
14

Old question, but for versions of TypeScript starting with 3.1, you can simply do the property assignment as you would in plain JS, as long as you use a function declaration or the const keyword for your variable:

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'"

Reference and online example.

eritbh
  • 726
  • 1
  • 9
  • 18
3

As a shortcut, you can dynamically assign the object value using the ['property'] accessor:

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

This bypasses the type checking. However, it is pretty safe because you have to intentionally access the property the same way:

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

However, if you really want the type checking for the property value, this won't work.

Rick Love
  • 12,519
  • 4
  • 28
  • 27
3

I can't say that it's very straightforward but it's definitely possible:

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

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

if you got curious this is from a gist of mine with the TypeScript/JavaScript version of Optional

thiagoh
  • 7,098
  • 8
  • 51
  • 77
  • this should be the best answer, it has both type and implementation – Acid Coder Jul 20 '22 at 03:43
  • How to type entire method in first member, I mean, if I just cant put a type instead `(): void;`, example: `express.RequestHandler: void;` or something like that to be the first member, so I don't put every function argument with its type... – Máxima Alekz May 07 '23 at 02:49
2

An updated answer: since the addition of intersection types via &, it is possible to "merge" two inferred types on the fly.

Here's a general helper that reads the properties of some object from and copies them over an object onto. It returns the same object onto but with a new type that includes both sets of properties, so correctly describing the runtime behaviour:

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

This low-level helper does still perform a type-assertion, but it is type-safe by design. With this helper in place, we have an operator that we can use to solve the OP's problem with full type safety:

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}`)
        }
    }
);

Click here to try it out in the TypeScript Playground. Note that we have constrained foo to be of type Foo, so the result of merge has to be a complete Foo. So if you rename bar to bad then you get a type error.

NB There is still one type hole here, however. TypeScript doesn't provide a way to constrain a type parameter to be "not a function". So you could get confused and pass your function as the second argument to merge, and that wouldn't work. So until this can be declared, we have to catch it at runtime:

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;
}
Daniel Earwicker
  • 114,894
  • 38
  • 205
  • 284
0

This departs from strong typing, but you can do

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

if you are trying to get around oppressive strong typing like I was when I found this question. Sadly this is a case TypeScript fails on perfectly valid JavaScript so you have to you tell TypeScript to back off.

"You JavaScript is perfectly valid TypeScript" evaluates to false. (Note: using 0.95)

WillSeitz
  • 75
  • 1
  • 2