7

Why is typescript ES6 not detecting that objects are not functions?

find: (collection: string, query: object, sortQuery = {}, cb?: Function)  => {
    socketManager.call('find', collection, query, sortQuery, cb);
}

Based off this function, you would assume that this would fail:

this._services._socket.methods.find('vendors', {type: 'repair'}, (errVen, resVen) => {}

Since there is no sortQuery object but instead a callback function. This is not giving me any type of error and means that typescript is allowing the callback as the object type.

How do I ensure this results in an error?

6
  • Does it actually infer the sortQuery as an object type? or is it any? Commented Nov 27, 2018 at 16:37
  • The same happens here, probably because functions are objects in JavaScript: typescriptlang.org/play/… Commented Nov 27, 2018 at 16:38
  • Same results with this: find: (collection: string, query: object, sortQuery: object, cb?: Function) => { socketManager.call('find', collection, query, sortQuery, cb); } Commented Nov 27, 2018 at 16:39
  • Any way to ensure 'Function' type and not an object? Commented Nov 27, 2018 at 16:40
  • 2
    Do you know the parameters and return types of the sortQuery? You could define it more specifically Commented Nov 27, 2018 at 16:44

2 Answers 2

5

With TypeScript Conditionals (TS v2.8), we can use Exclude to exclude Functions from the object type using Exclude<T, Function>:

let v = {
  find: <T extends object>(collection: string, query: object, sortQuery: Exclude<T, Function>, cb?: (a: string, b: string) => void) => {
  }
}

// Invalid
v.find('vendors', { type: 'repair' }, (a, b) => { })
v.find('vendors', { type: 'repair' }, 'I am a string', (a, b) => { })

// Valid
v.find('vendors', { type: 'repair' }, { dir: -1 })
v.find('vendors', { type: 'repair' }, { dir: -1 }, (a, b) => { })

A default parameter value can then be set like this:

sortQuery: Exclude<T, Function> = <any>{}

As you can see in the image below, errors are thrown for the first two calls to find, but not the second two calls to find:

TypeScript Exclude

The errors that then display are as follows:

  • [ts] Argument of type '(a, b) => void' is not assignable to parameter of type 'never'. [2345]
  • [ts] Argument of type '"I am a string"' is not assignable to parameter of type 'object'. [2345]
Sign up to request clarification or add additional context in comments.

2 Comments

This is a great solution. So one thing I didn't mention is that I have a script which is running through all my server functions and parsing through the files to grab the function names and the parameters to generate the functions on the client side. Would it be ok if I changed my script to do this on all functions?
find: <T extends object>(collection: string, query: object, sortQuery: Exclude<T, Function> = <any>{}, cb?: Function) => { socketManager.call('find', collection, query, sortQuery, cb); }, findWithOptions: (collection: string, query: object, options: PaginationOptions, cb?: Function) => { socketManager.call('findWithOptions', collection, query, options, cb); }, insertDocument: (collection: string, document: object, cb?: Function) => { socketManager.call('insertDocument', collection, document, cb); },
5

Objects and functions are fundamentally the same thing, but typings help us disambiguate between their functionality.

Consider this:

const foo1: { (): string } = () => "";

The variable foo has a type of object, but that object is callable ... it's a function, and it can indeed be called, but you can't go setting a property called bar on it.

foo1(); // This works
foo1.bar = 5; // This, not so much.

Also consider this:

const foo2: { bar?: number; } = {};

The variable foo has a property called bar on it. That property can be set, but the object can't be called, as it's not typed as callable.

foo2.bar = 5; // This works
foo2(); // This, not so much.

So, lets have a look at your original typing:

sortQuery = {}

sortQuery is an object, but that's all that we know about it. It doesn't have any properties, it's not callable, it's just an object.

We've already seen that a function is an object, so you can assign a function to it just fine. But, you won't be able to call it, as it's not defined as callable.

const sortQuery: {} = () => ""; // This works.
sortQuery(); // This, not so much.
sortQuery.bar = 5; // Nor this.

If you have full control of the source code, then one way to solve this is to move from multiple parameters, to a single parameter with named properties:

const foo = (params: { collection: string, query: object, sortQuery: {}, cb?: Function }) => { };

foo({ collection: "", query: {}, sortQuery: {} }); // Fine
foo({ collection: "", query: {}, sortQuery: {}, cb: () => { } }); // Fine
foo({ collection: "", query: {}, cb: () => { } }); // Not Fine

This removes any ambiguity by requiring names, rather than relying on position in the function call.

2 Comments

This is also a good solution to use objects. Unfortunately this would take a considerable amount of time to rework all the calls / create a script to fix all this. Good answer though.
Rather use a destructed parameter fnName( { p1, p2, p3 } : { p1: string, p2: number, p3: boolean })

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.