Open ooflorent opened 8 years ago
How is the {[_: string]: Cs...}
type used? I'm unclear about what you're asking for here.
Well, the following is pseudo code to highlight the main idea:
class Foo {}
class Bar {}
class Baz {}
class Qux {}
var entity1 = createEntity({foo: Foo, bar: Bar, baz: Baz})
var entity2 = createEntity({foo: Foo, qux: Qux})
Basically createEntity
accepts an object where each value is a Class
.
The resulting entity types would be:
type Entity1 = {
foo: ?Foo;
bar: ?Bar;
baz: ?Baz;
}
type Entity2 = {
foo: ?Foo;
qux: ?Qux;
}
I think createEntity
signature would be something like:
function createEntity<...Cs>(cs: {[_: string]: Class<Cs...>}): {[_: string]: ?Cs...}
I want to avoid any
to keep the code strictly typed but would like it to be generic.
Any thoughts on how to achieve it?
What would the implementation of createEntity
be?
I've created of what I'm trying to achieve. https://gist.github.com/ooflorent/84260ef9aa8498fb63b1
Example usage:
const em = createManager({transform: Transform2D, body: Body, sprite: Sprite})
const entity = em.create()
In the above example, entity type would be defined as:
declare class Entity {
transform: Transform2D;
body: Body;
sprite: Sprite;
}
Calling createManager
with another object shape would recompile an Entity
class and shape it according createManager
argument.
I have also wanted variadic generics, when writing a function that behaves similarly to Promise.all
. I ended up writing non-variadic type-safe versions:
static all2<A, B, U>(
resultA: Result<A>,
resultB: Result<B>,
func: (a: A, b: B) => U
): Result<U> {
return Result.all([resultA, resultB])
.map(a => func(a[0], a[1]));
}
static all3<A, B, C, U>(
resultA: Result<A>,
resultB: Result<B>,
resultC: Result<C>,
func: (a: A, b: B, c: C) => U
): Result<U> {
return Result.all([resultA, resultB, resultC])
.map(a => func(a[0], a[1], a[2]));
}
// ...etc
Thanks @rjbailey. The Promise.all
example is a bit easier to wrap my head around. We are actually kicking around some ideas to make typing these kinds of APIs easier, but it's still very much in the primordial phase.
@ooflorent, do you think Promise.all
is similar to your issue? I haven't spent a lot of time trying to grok the gist you shared.
@samwgoldman Yes it is similar.
@samwgoldman I've found a way more descriptive use case.
How would you write ES7 typed objects type definitions?
Here is an example using StructType
:
const Point2D = new StructType({ x: uint32, y: uint32 })
let p2 = Point2D({x: 10, y: 20})
let x = p2.x // ok
let z = p2.z // TypeError
const Point3D = new StructType({ x: uint32, y: uint32, z: uint32 })
let p3 = Point3D({x: 10, y: 20}) // TypeError
I've been thinking about variadic generics a lot lately. One problem I ran into is that Kefir.combine has a very similar type signature to Promise.all, but Flow's support for Promise.all is hard-coded, so Kefir.combine couldn't be properly typed. These functions' type signatures share some similarities to createEntity too. I think I found a solution that looks nice, though I don't know if it really aligns with how Flow works internally.
// $Wrapped<{a: number, b: string}, Foo> refers to the type {a: Foo<number>, b: Foo<string>}
declare function createEntity<T: Object>(classes: $Wrapped<T, Class>): T;
// (Partial) Bluebird Promise.props: http://bluebirdjs.com/docs/api/promise.props.html
declare function props<T: Object>(obj: $Wrapped<T, Promise>): Promise<T>;
// $Tuple refers to some specific tuple of types like [number, string, boolean].
// $Wrapped<[number, string, boolean], Foo> refers to the type [Foo<number>, Foo<string>, Foo<boolean>].
// Promise.all:
declare function all<T: $Tuple>(arr: $Wrapped<T, Promise>): Promise<T>;
// ~ is an operator where
// type NumberStringTyple = [number, string];
// type NumberStringBooleanDateTuple = NumberStringTyple~[boolean, Date];
// Kefir.combine:
declare function combine<O: $Tuple>(obss: $Wrapped<O, Observable>): Observable<O>;
declare function combine<O: $Tuple, C>(obss: $Wrapped<O, Observable>, combinator: (values: O) => C): Observable<C>;
declare function combine<P: $Tuple, P: $Tuple>(obss: $Wrapped<O, Observable>, passiveObss: $Wrapped<P, Observable>): Observable<O~P>;
declare function combine<O: $Tuple, P: $Tuple, C>(obss: $Wrapped<O, Observable>, passiveObss: $Wrapped<P, Observable>, combinator: (values: O~P) => C): Observable<C>;
I've also been thinking of the type of the compose function as implemented by ramda, lodash, multiple transducer libs, etc, which can take any number of functions, and returns a function that takes the input type of the last function and returns the output type of the first function. I didn't really come up with a generic solution for it, but it seems like everyone implements it about the same way (okay, there are variants where the first called function is allowed to take 1 parameter and some where it can take any number of parameters) and without binding it to specific types (promises, observables, etc), so maybe it deserves its own special type like Promise.all currently has:
declare var compose: $Compose;
Well okay, I also came up with this solution to the variadic generics problem. It's ... not fully developed, but possibly a lot more generic, but it's also asking a lot more out of Flow and not as readable. Maybe someone will see this and realize it's exactly what Flow needs, or it's exactly what Flow doesn't need.
declare function all<T>(arr: T): Promise<$Reduce<T, (C,N) => C~[N], []>>;
declare function compose<T>(...args: T): $Reduce<T, ((a: MID) => OUT, (a: IN) => MID) => (a: IN) => OUT>;
@AgentME I have a proof of concept implementation, that makes this possible, looks like this:
declare function all<T>(arr: T): Promise<$TupleMap<T, <T>(t: Promise<T>) => T>>;
I think I have another use-case for variadic generics, as I need to type a function so that it has a covariant input parameter, but the Function
type doesn't currently support generics -- presumably because there's no support for variadic generics.
Here's some code that highlights the need for this:
class C<Type> {
funcs: Array<(t: any) => Type>; // we should be using `Type` instead of `any` here
m<T: Type>(t: T, f: (t: T) => T): T {
this.funcs.push(f);
return f(t);
}
}
type X = {type: 'X', x: number};
type Y = {type: 'Y', y: number};
const x: X = {type: 'X', x: 1};
const c: C<X | Y> = new C();
const f = (v: X): X => v;
c.m(x, f);
If I change the definition of funcs
from Array<(t: any) => Type>
to Array<(t: Type) => Type>
then I get an error because function input parameters are contravariant, yet my functions require various sub-types of Type
.
At present, the funcs
member variable is effectively typed like this (if the Function
type supported generics):
Array<Function<-Type, +Type>>;
whereas I need it to be typed as:
Array<Function<+Type, +Type>>;
I was able to solve my own particular issue by using the (undocumented) $Subtype<T>
type, so that I simply changed this line:
funcs: Array<(t: any) => Type>; // we should be using `Type` instead of `any` here
to this:
funcs: Array<(t: $Subtype<Type>) => Type>;
and all was well in the world again :smile:
I need this too, a stripped down version of my use case is:
const prepend = (arg, fn) => (...rest) => fn(arg, ...rest)
It's relatively straightforward if you use $ObjMap
.
declare function createEntity<T: {}>(obj: T): $ObjMap<T, <X>(klass: Class<X>) => X>;
See working example here.
@szdavid92 Here's something that seems to work in your case:
const prepend = <T, Rest: $ReadOnlyArray<mixed>, R>(
arg: T,
fn: (first: T, ...rest: Rest) => R
): ((...rest: Rest) => R) => (...rest: Rest) => fn(arg, ...rest);
Code here
Flow technically has some weak incomplete support for variadic generics through Tuple types.
Tuple types are a subType of $ReadOnlyArray
and we can use that to our advantage some of the times.
For reference, here is typescript's proposal for the same thing:
this has landed on typescript 3
@sibelius it isn't, https://github.com/Microsoft/TypeScript/issues/5453 is still open
It would be nice if whatever we end up with worked with $Pred
. Currently $Pred
is typed in the following way:
$Pred<1> => (x_0: any) => mixed
$Pred<2> => (x_0: any, x_1) => mixed
...
It would be nice if $Pred
could act more like:
$Pred<...Types> => (...args: Types) => mixed
Where Types
is a tuple corresponding to the parameter types.
It would be nice if
$Pred
could act more like:$Pred<...Types> => (...args: Types) => mixed
Where
Types
is a tuple corresponding to the parameter types.
More generally, I've got a use case where I want to statically type generics for functions, where the the function can have any number of arguments. e.g.
function logFunction<Args: $Tuple, RetVal>(
name: string, f: (...Args) => RetVal
): (...Args) => RetVal {
return (...(args: Args)) => {
console.log(`calling ${name} with arguments: ${args.toString}`);
return f(args);
}
}
function promisifyfunction<Args: $Tuple, RetVal>(
f: (...Args) => RetVal
): (...Args) => Promise<RetVal> {
return (...(args: Args)) => Promise(f(...args));
}
I was wondering if Flow supports variadic generics or would support them. I'm trying to achieve the this but I can't figure out how to do it.
The main idea is to declare a function that accepts
{[_: string]: Class<Cs...>}
and returns{[_: string]: Cs...>
. I tried using different flow types but it seems currently impossible to achieve it.Edit: Using an union type could probably solve this but it raises another problem.