I have a JS library where I make heavy use of adding properties to functions.
In the playground I was trying out the concept and I can't get it to type check properly.

/* @flow */
type Task<FN> = FN & {
lastError: ?Error
}
function task <T>(inner: T): Task<T> {
function wrapped () {
try {
return (inner: any).apply(this, arguments)
} catch (err) {
wrapped.lastError = err
}
}
return (wrapped: any) // only way to get it to not fail here
}
const t = task((str) => 'Hello ' + str.substring(1))
console.log(t('sworld')) // good
console.log(t(123)) // type error, also good, works as expected
console.log(t.hahahBugOff(123)) // ... what? No error?
Am I doing something wrong or is this simply not possible?
I believe this is what you're looking for?
You're returning a function that takes a subtype of the original input and an optional supertype of the output.
type Task<Fn> = Fn & {
lastError?: Error,
}
function task<
Input,
Output,
InputSubtype: Input,
OutputSubtype: Output,
>(inner: Input => OutputSubtype): Task<InputSubtype => ?Output> {
return function wrapper(...rest) {
try {
return inner.apply(this, rest)
} catch (err) {
wrapper.lastError = err
}
}
}
Also, console.log can take an Array<any>, so it's not really a great way to test types.
log(...data: Array<any>): void;
https://github.com/facebook/flow/blob/master/lib/core.js#L852
Regarding the last line of your code, it looks like Flow just doesn't type check properties on functions by default. For example, this does not error:
declare var foo: string => number
foo.bar.baz
You can be more explicit than the default behavior though...
type Task<T, U> = {
(T): U,
lastError?: Error,
}
function task<
Input,
Output,
InputSubtype: Input,
OutputSubtype: Output,
>(inner: Input => OutputSubtype): Task<InputSubtype, ?Output> {
return function wrapper(...rest) {
try {
return inner.apply(this, rest)
} catch (err) {
wrapper.lastError = err
}
}
}
const t = task((str) => 'Hello ' + str.substring(1))
console.log(t('sworld')) // good
console.log(t(123)) // type error, also good, works as expected
console.log(t.hahahBugOff(123)) // ... what? No error?
21: const t = task((str) => 'Hello ' + str.substring(1))
^ property `substring`. Property not found in
21: const t = task((str) => 'Hello ' + str.substring(1))
^ Number
25: console.log(t.hahahBugOff(123)) // ... what? No error?
^ property `hahahBugOff`. Property not found in
25: console.log(t.hahahBugOff(123)) // ... what? No error?
^ object type
Your last example seems to be more of what I want, but I need the Task to take the signature of the input function. Task<T, U> means a function with 1 parameter, whereas I want it to be based on whatever is passed in. Is that possible?
If you wondering if you can express { (A): B, prop: C } as an intersection Fn & { prop: C } & retain all the type information / safety my example Task<InputSubtype, ?Output> provides, I believe the answer is no.
You may be able to do this with classes though...
I say classes (or interfaces), because it would allow you to express a "callable" object. Something like...
class Task<T> {
static lastError: void | Error;
constructor: T;
}
If this works for you, I'd be curious. Please post an example. ๐
I basically want to add typings to https://github.com/jeffijoe/mobx-task - it works by returning a wrapped function with additional properties. ๐
@mwalkerwells class didn't do the trick I'm afraid.
Your last example seems to be more of what I want, but I need the Task to take the signature of the input function. Task
means a function with 1 parameter, whereas I want it to be based on whatever is passed in. Is that possible?
I extented @mwalkerwells 's example to work with generalized list of arguments. See if this works for you.
/* @flow */
type Task<Fn> = Fn & {
lastError?: Error,
}
function task<
Input: $ReadOnlyArray<*>,
Output,
OutputSubtype: Output
>(inner: (...rest: Input) => OutputSubtype): Task<(...rest: Input) => ?Output> {
return function wrapper(...rest) {
try {
return inner.apply(this, rest)
} catch (err) {
wrapper.lastError = err
}
}
}
That loses type checking on the function properties.
I can get this working in TypeScript:
interface ITask {
lastError?: Error
}
interface ITaskFactory {
<T extends Function>(fn: T): T & ITask;
}
const task: ITaskFactory = function (inner: Function) {
const wrapped: any = function wrapped() {
try {
return inner.apply(this, arguments)
} catch (err) {
(wrapped as any).lastError = err
}
}
wrapped.lastError = null
return wrapped
}
const t = task((str: string) => str.length)
t('hello') // good
t(123) // error, good
console.log(t.lastError) // good
console.log(t.haha) // error, good
Trying to do the same in Flow:
interface Task {
lastError: ?Error
}
function task<T: Function>(inner: T): T & Task {
function wrapped () {
try {
return inner.apply(this, arguments)
} catch (err) {
wrapped.lastError = err
throw err
}
}
wrapped.lastError = null
return (wrapped: any)
}
const t = task((str) => str.length)
t('Hello') // good
t(123) // error, good
console.log(t.lastError) // good
console.log(t.haha) // no error, not good
So it seems TypeScript can figure it out. Any ideas?
That loses type checking on the function properties.
can you send me an example?
Also, translating your Typescript to Flow syntax seems to do what you want:
LINK
--
Correction, the output type isn't correctly typed. Looking into it.
Here, this works. Like I did in my previous example:
type ITask<I: $ReadOnlyArray<mixed>, O> = {
(...r: I): O,
lastError?: Error
}
function task <I: $ReadOnlyArray<mixed>, O>(inner: (...r: I) => O): ITask<I, O> {
const wrapped: any = function wrapped() {
try {
return (inner: any).apply(this, arguments)
} catch (err) {
(wrapped:any).lastError = err
}
}
wrapped.lastError = null
return wrapped
}
const t = task((str: string) => str.length)
t('hello') // good
t(123) // error, good
t(true) // error, good
console.log(t.lastError) // good
console.log(t.haha) // error, good
Although, to actually be type-safe, you should change ITask's type to this:
type ITask<I: $ReadOnlyArray<mixed>, O> = {
(...r: I): ?O,
lastError?: Error
}
This is because the function task will sometimes return undefined even if the original function never did.
Dang, that's a mouthful! ๐
Thanks! Do you think you could walk me through the generics part?
Sure,
The important part to understand is that Tuple types are a subtype of $ReadOnlyArray<mixed>.
So:
function task <
I: $ReadOnlyArray<mixed>, // I is a generic "LIST" of arguments.
O // O is the Return type of your input function.
>(
inner: (...r: I) => O // since I was a list, we can use the spread operator to use it.
): ITask<I, O> { ... }
Does that make sense?
Essentially,
instead of doing something like (...args: Array<mixed>) => O where we take any list of arguments, we are taking a specific list.
Yeah, thanks!
So how come Flow wouldn't be able to do this the same way TypeScript does? From what I hear Flow prides itself by being more strict, so it surprised me that in this case, TypeScript was more strict. ๐ผ
Well the problem is with the way the & works. It's a little more nuanced in Flow. This is the reason the object spread was added to Flow.
Alright, thank you very much! Should I leave this open for potential future improvements to this use case or is this the official way to do this?
Let's leave this open as a Function & Object expression should probably work too.
@jeffijoe Saw https://github.com/facebook/flow/issues/2966 so I thought I'd chime in here.
/* @flow */
type Task<Fn> = {
$call: Fn,
lastError?: Error,
}
function task <T: Function>(inner: T): Task<T> {
return ((function wrapped(...rest) {
try {
return inner.apply(this, rest)
} catch (err) {
wrapped.lastError = err
}
}): any)
}
const sayHello: (string => string) = value => 'Hello ' + value.substring(1)
const t = task(sayHello)
t('sworld') // โ
t(123) // โ
t.hahahBugOff(123) // โ
Still have one any that I can't get rid of, but works like you'd expect & it's simple. $call is a private API, so who knows how it'll work in the future...
@mwalkerwells if it's private then I'm not even gonna bother. ๐
Anything marked with $ is technically private, but they're a necessity with Flow.
Sure but if there's talk of removing it?