Skip to content

Functions should not be assignable to objects #27278

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
3 of 4 tasks
KasparEtter opened this issue Sep 21, 2018 · 6 comments
Closed
3 of 4 tasks

Functions should not be assignable to objects #27278

KasparEtter opened this issue Sep 21, 2018 · 6 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@KasparEtter
Copy link

KasparEtter commented Sep 21, 2018

Search Terms

function, object, type compatibility, assignment, generic constraint

Suggestion

I was surprised by the following behavior of TypeScript (as of version 3.0.3):

let o: object;
o = { p: 1 }; // fine [as expected]
o = 'a'; // error TS2322: Type '"a"' is not assignable to type 'object'. [as expected]
o = 1; // error TS2322: Type '1' is not assignable to type 'object'. [as expected]
o = (x: number) => x; // fine [UNEXPECTED!]
o(1); // error TS2349: Cannot invoke an expression whose type lacks a call signature. Type '{}' has no compatible call signatures. [as expected]

I searched everywhere for documentation and explanation of this behavior. The closest I could find is

object is a type that represents the non-primitive type, i.e. any thing that is not number, string, boolean, symbol, null, or undefined.

from the official handbook, which kind of implies that functions are not excluded. This behavior is nevertheless surprising given the fact that even JavaScript distinguishes between object and function types (at least in case of typeof.) (Not being a JavaScript expert myself, there does seem to be a difference in JavaScript, though: In case of let f = a => a; f.p = 1;, f.p evaluates to 1, whereas in case of let v = 1; v.p = 1;, v.p evaluates to undefined. This might explain why TypeScript handles functions the way it does, however, it shouldn't be relevant how JavaScript handles this, in my opinion, because such assignments don't work in TypeScript anyway.)

My suggestion is to make functions non-assignable to the object type (which can but doesn't have to include introducing a new type function (or something similar if the collision with the keyword is to be avoided) for all types that have a call signature).

Use Cases

The reason I stumbled upon this behavior is that I thought about constraining the generic type of a React higher-order component with object (as in function load<LoadedProps extends object, ProvidedProps extends object = {}>(…) {…} which loads some properties from the backend but still requires other properties to be provided by the caller). At least to me, it makes sense that properties have to be objects but with the current version of TypeScript you can still write load<(a: string) => string>(…) (though not load<string>(…)).

Additional Suggestion

There was also another reason why I thought about constraining the generic type: I'm using keyof LoadedProps in the code, which also only makes sense on objects – at least to me. Using the tooltip of Visual Studio Code, keyof has the following behavior:

type A = keyof { p: any }; // type A = "p" [expected]
type B = keyof string; // type B = number | "toString" | "charAt" | "charCodeAt" | "concat" | "indexOf" | "lastIndexOf" | "localeCompare" | "match" | "replace" | "search" | "slice" | "split" | "substring" | "toLowerCase" | ... 27 more ... | "padEnd" [unexpected because methods like toString also exist on objects]
type C = keyof number; // type C = "toString" | "valueOf" | "toFixed" | "toExponential" | "toPrecision" | "toLocaleString" [equally unexpected but at least consistent]
type D = keyof (string | number); // type D = "toString" | "valueOf" [also unexpected but makes sense given the above]
type E = keyof ({ p: any } | undefined); // type E = never [makes sense]
type F = keyof (string | undefined); // type F = never [makes also sense]
type G = keyof (any | undefined); // type G = string | number | symbol [seriously?! Strange but not really the focus of this issue]
type H = keyof ((a: any) => a); // type H = never [makes sense]

Wouldn't it make sense to support keyof only for object types?

type Partial<T> = {
    [P in keyof T]?: T[P];
};

could (respectively with this second suggestion then should) result in a compile-time error and would have to replaced by

type Partial<T extends object> = {
    [P in keyof T]?: T[P];
};

Feedback

If this has been asked, discussed or explained before, please refer me to the corresponding conversation or documentation. If this issue hasn't been raised before, do my suggestions make sense or is this behavior intended (and for what reasons)?

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript / JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. new expression-level syntax)

This wouldn't be a breaking change in existing TypeScript / JavaScript code

I guess it doesn't have to be a breaking change in existing code if, instead of redefining object, we introduce a new type objectWithoutFunctions (or something more elegant). (The second suggestion breaks existing code like in the given example but maybe it could still be considered for TypeScript 4.)

@RyanCavanaugh
Copy link
Member

Much like how Dogs are Mammals, Functions are objects in JavaScript. They are instanceof Object, you can pass them to Object.keys, you can call defineProperty with them, you can assign properties to them, etc etc etc. There's nothing that's valid to do with a regular object that isn't valid to do with a function; it is truly a subtype.

You can write something like this to achieve something close to the desired effect:

type ObjectButNotFunction = object & { prototype?: never; };

function fn(obj: ObjectButNotFunction) { }
// OK
fn({});
// Error
fn(() => { });

@KasparEtter
Copy link
Author

Thanks for the quick response and the hint how to define an ObjectButNotFunction type! I will gladly use this in my code.

What do you think about my second suggestion? The behavior of keyof string seems strange to me (given that you don't get methods like toString() with keyof { p: any }). And shall I report keyof (any | undefined) = string | number | symbol as a bug or is this indeed intended?

@KasparEtter
Copy link
Author

Given your explanation, I tried the following, which works:

type FunctionWithProperty = (() => void) & { p?: number };
const f: FunctionWithProperty = () => { return; };
f.p = 1;

keyof FunctionWithProperty is then "p", which is nice.

How would you create an instance if the key p wasn't optional?

@weswigham
Copy link
Member

weswigham commented Sep 21, 2018

How would you create an instance if the key p wasn't optional?

As of 3.1, simply

const f= () => { return; };
f.p = 1;
const y: (() => void) & { p: number } = f; // Works!

@NN---
Copy link

NN--- commented Sep 22, 2018

@KasparEtter keyof {p:any;} works as designed.
http://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html

Seems like 'keyof string' just returns all members of 'String' interface.
Not sure whether it is desired or just implementation details.
You can see this in playground

And here everything is correctkeyof (any | undefined) = string | number | symbol
Check changes of keyof in 2.9
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-9.html

keyof now returns string | number | symbol.

@KasparEtter
Copy link
Author

Thanks a lot for all your responses! Since there exist enough open issues already, I close this one now with the following remarks and conclusions for all future readers:

  • As usual, JavaScript is to blame for my headache whereas TypeScript is awesome. Functions are indeed objects (as explained here), they are both instanceof of Function and Object, and I was simply misled by typeof. While I still think that TypeScript could (without implying should) handle functions differently, I see and understand why TypeScript functions the way it does.
  • As suggested by @RyanCavanaugh, you can achieve what I as looking for with type ObjectButNotFunction = object & { prototype?: never; };. If I'm not mistaken, this works only in TypeScript 3, though. In our project, where I'm stuck with TypeScript 2.9.2, unfortunately, I can still assign a function to a variable of that type. Moreover, the construction of this type is counterintuitive (at least to me) and should ideally be documented in the handbook as objects also have a prototype.
  • I took the keyof examples from my Visual Studio Code running TypeScript 2.9.2 (again, my bad, sorry). Using the excellent Playground with tooltip support, which I wasn't aware of, keyof (… | undefined) is now consistently ignoring the undefined part (see the link to the Playground). (It would be great if one could choose an older TypeScript version in the Playground but this is again a different topic!)
  • Supporting keyof only for types that extend object might still be a worthwhile suggestion for the TypeScript team but this was not the focus of this issue. (What I mean is not that keyof should return never for all non-objects but rather that keyof number, for example, should be a compile-time error and thus also Partial<number> – instead of being number, which I don't get at all given what keyof number returns (see this Playground).)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

4 participants