3

After reading this answer to the question How is jQuery's $ a function and an object?, it got me thinking. How would you define this kind of type in typescript?

In regular JS this is completely valid:

var f = function() { alert('yo'); }
f.foo = "bar";

alert(f.foo); // alerts "bar"
f();          // alerts "yo"

However in typescript, f.foo would throw an error, Property 'foo' does not exist on type '() => void'.

While you could achieve a similar result using bracket notation:

var f = function() { alert('yo'); }
f['foo'] = "bar";

alert(f['foo']); // alerts "bar"
f();          // alerts "yo"

This would entirely bypass the type system, and there for the type safety, of typescript.

Is there a way of implementing this type of functionality without violating the type safety of typescript?

Olian04
  • 6,480
  • 2
  • 27
  • 54
  • 1
    Maybe you can find a hint in [DefinitelyTyped/types/jquery/index.d.ts](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/924fafffc09cfeb0267573af2c847cdbfcfa464d/types/jquery/index.d.ts), I think they do exactly that, but I didn't take a closer look at it. – t.niese Aug 24 '17 at 12:30
  • @t.niese They do, however that file is 8000 rows, and extracting the few rows that define this particular behavior is made even harder when i don't even know what I'm looking for. – Olian04 Aug 24 '17 at 12:36
  • But it is at least a starting point ;). Just in case that you do not get any answer that helps, you would at least have this link where you might find a solution. I would also need to look through the whole document to find that information, because I don't know it either. – t.niese Aug 24 '17 at 12:38
  • @t.niese Yea, thank you. Lets hope I wont have to resort to that. – Olian04 Aug 24 '17 at 12:39
  • What kind of functionality do you want? Some sort of factory function like jQuery's `$` which gets a CSS selector and returns wrapped DOM objects with jQuery functions attached? It is not clear what kind of functionality do you want. – Dmitry Aug 24 '17 at 13:31
  • @Dmitry I don't really care for any of the implementation details, that I can do on my own. What I'm curious about is how you define a type in ts that's both an object and a function at the same time. – Olian04 Aug 24 '17 at 13:43
  • This question already has answers, but the [Object Types section of the TypeScript spec](https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md#13-object-types) has a bit about JQuery and how the interface both has properties and is callable like a function. – jcalz Aug 24 '17 at 17:12
  • You really should think about separating code from data, not to mix it. –  Aug 24 '17 at 19:35

2 Answers2

3

You can define this kind of type by specifying an invocation signature on an interface:

interface MyFoo {
    (): void;
    foo: string;
}

And you can initialize the object by casting the function to any before assigning it to the variable:

let f: MyFoo = function() { alert('yo'); } as any;
f.foo = 'bar';
x();

Playground link


As an aside, the JQueryStatic type (which is the type of the $ variable) is defined similarly in the JQuery defintions:

interface JQueryStatic<TElement extends Node = HTMLElement> {
    ...
    Deferred: JQuery.DeferredStatic;
    ...
    (html: JQuery.htmlString, ownerDocument_attributes: Document | JQuery.PlainObject): JQuery<TElement>;
    ...
    (selector: JQuery.Selector, context: Element | Document | JQuery | undefined): JQuery<TElement>;

(Can't put the links inline, so here they are)

However, this doesn't really help, as your question is also how to initialize a variable with this type. The JQuery library doesn't have that problem, because it isn't written in Typescript.

Zev Spitz
  • 13,950
  • 6
  • 64
  • 136
  • How would you use this interface? – Olian04 Aug 24 '17 at 15:32
  • @torazaburo To be fair, having to use `any` for what should be a straightforward initialization leaves me feeling slightly uncomfortable. – Zev Spitz Aug 26 '17 at 19:04
  • @ZevSpitz thank you. And I agree, having it use `any` feels dirty, but having the option of using interfaces instead of intersection types, kind of makes up for it in my opinion. – Olian04 Aug 26 '17 at 20:00
  • @Olian04 Why do you prefer an interface over an intersection type, aside from 1) any function being assignable to `Function`, not just a specific signature; and 2) the `foo` property is optional, and therefore might be `undefined` (as I noted in the comments [here](https://stackoverflow.com/questions/45861592/how-would-you-define-a-type-in-typescript-thats-both-an-object-and-a-function-a/45865475#comment78756353_45865270) and [here](https://stackoverflow.com/questions/45861592/how-would-you-define-a-type-in-typescript-thats-both-an-object-and-a-function-a/45865475#comment78756652_45865270)? – Zev Spitz Aug 26 '17 at 21:33
  • @ZevSpitz other than those two reasons, it's mostly personal preference. It feels more easily maintainable, and more self documenting. – Olian04 Aug 27 '17 at 08:19
1

You can use & to define a variable with multiple types. It's called an "Intersection Type". More informations in the official docs.

Here's how to use it in your example :

let f: Function & {foo?: String} = function() { alert('yo'); }
f.foo = "bar";

alert(f.foo); // alerts "bar"
f();          // alerts "yo"

Playground Link

Here, the type of f is Function & {foo?: String} because it's both a Function, and also {foo?: String}. The ? is required here, as you initialize it as only a Function, and only then you assign a value to foo.

You can see in the Playground that the code compiles, and even gives the same JavaScript code you started with, validating that this is the correct syntax.

Kewin Dousse
  • 3,880
  • 2
  • 25
  • 46
  • Using `Function` allows any function signature to be assigned to `f`: [playground](https://www.typescriptlang.org/play/#src=let%20f%3A%20Function%20%26%20%7B%20foo%3F%3A%20String%20%7D%20%3D%20(first%3A%20string%2C%20second%3A%20string)%20%3D%3E%20alert(first%20%2B%20second)%3B%20). AFAIK there is no way to specify a function type with a given signature, aside from an interface. – Zev Spitz Aug 26 '17 at 18:48
  • [Playground](https://www.typescriptlang.org/play/#src=var%20creator%20%3D%20()%20%3D%3E%20%7B%0D%0A%20%20%20%20let%20f%3A%20Function%20%26%20%7B%20foo%3F%3A%20string%20%7D%20%3D%20function%20()%20%7B%20alert('yo')%3B%20%7D%0D%0A%20%20%20%20f.foo%20%3D%20%22bar%22%3B%0D%0A%20%20%20%20return%20f%3B%0D%0A%7D%3B%0D%0A%0D%0Aalert(creator().foo.length)%3B). – Zev Spitz Aug 26 '17 at 18:59
  • Also, `foo` is typed as possibly undefined; it's not an issue here because Typescript's flow analysis sees the assignment for this particular object, and determines that `foo` is `string` and not `string | undefined`. Once outside the local context (e.g. if returned from a function), and using the `strictNullChecks` option, `foo` is treated as `string | undefined`. (playground link above). – Zev Spitz Aug 26 '17 at 19:04