Accept which array method by caller in typescript - node.js

Background:
The only difference between the following functions is:
createGrid uses map (taking a callback returning type T) and returns T[][]
visitGridCoordinates uses forEach (taking a callback returning type void) and returns void.
const createGrid = <T>(width: number, height: number, callback: (x: number, y:number) => T) =>
Array
.from(Array(width).keys())
.map(
(x: number) => Array
.from(Array(height).keys())
.map((y: number) => callback(x, y))
);
const visitGridCoordinates = (width: number, height: number, callback: (x: number, y:number) => void) =>
Array
.from(Array(width).keys())
.forEach(
(x: number) => Array
.from(Array(height).keys())
.forEach((y: number) => callback(x, y);)
);
Question:
Is there a way to create a wrapper function over these that takes which method to use as an argument? I tried several things but kept running into problems.
const createGrid = verbGrid<string>(Array.prototype.map); // or ('map') alternately
const visitGridCoordinates = verbGrid(Array.prototype.forEach); // or ('forEach') alternately
const verbGrid = ???
I tried writing such a function, then used "infer parameter types from usages" and the result almost works:
const verbGrid = <U>(arrayMethod: (callbackfn: (value: any, index: number, array: any[]) => void, thisArg?: any) => void) => (
width: number,
height: number,
callback: ((x: number, y:number) => U) | ((x: number, y:number) => boolean)
) => {
const result: void | U[][] = arrayMethod.call(
Array.from(Array(width).keys()),
(x: number) => arrayMethod.call(
Array.from(Array(height).keys()),
(y: number) => callback(x, y)
)
)
return result;
};
But when I try to use createGrid its return type is always only void. And of course it is, because the type of arrayMethod says it returns void. When I try using unioned types for the entire type descriptor or just for the return value, various different errors occur.
I played around with using varof as in varof Array<number>["map"] | varof Array<number>["forEach"] to get more explicit with the types and avoid having to re-describe both method types. However I still had no luck (and I notice the map version doesn't specify U anywhere and I don't know how to do that).
Is this even possible, and if so, do you mind helping me understand where I'm going wrong?
Various results I've had:
The result of verbGrid being type unknown.
No errors in verbGrid but being unable to assign the result to a variable of type U[][] at the call site.
Passing in 'map' and 'forEach' as strings and using them as [method] (instead of doing method.call).
I tried to simplify things a little like so:
type ValueReturning = <T, U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any) => U[];
type VoidReturning = <T>(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any) => void;
class Enumerable {
static range(start: number, count: number) {
return Array.from(Array(count).keys()).map(index => start + index);
}
}
But this doesn't work at all ("Type 'ValueReturning' is not generic"):
const verbGrid = <U>(arrayMethod: ValueReturning<number, U> | VoidReturning<number>) => (
Appreciate any guidance.
Note: I am new to TypeScript but not new to Generics (from C#).
Addendum
In case it helps, here are the call sites to these two functions.
this._grid = createGrid(width, height, (x: number, y: number) => {
const cell = new Cell<string>(x, y);
this._cells.add(cell);
return cell;
});
visitGridCoordinates(width, height, (x, y) => {
getNeighborCoordinates(width, height, x, y)
.forEach(([neighborX, neighborY]) => {
this.grid[x][y].neighbors.add(this._grid[neighborX][neighborY]);
});
});
It's fine to suggest alternate methods for achieving the same goal, but I really was more interested in learning TypeScript better than I am in fixing this code to be perfectly optimal. (I spent a couple of hours creating a Boggle/Wordament solver last night—it works great and was fun to build.)

I ran into some annoying difficulties with this, like you did. One type function that I found useful is this:
type ArrayUnlessVoid<T> = T extends void ? void : Array<T>;
It's a conditional type that turns T into T[] unless T is void, in which case it's just void. We might want this because it unifies what map() and forEach() do: they each take a callback that returns a T and returns an ArrayUnlessVoid<T>.
Okay, here is the typing for verbGrid():
const verbGrid =
<R, O>(arrayMethod: (
callbackfn: (value: any, index: number, array: any[]) => R,
thisArg?: any
) => O) =>
(width: number, height: number, callback: ((x: number, y: number) => R)) => {
const outerArrayMethod = arrayMethod as any as (
callbackfn: (value: any, index: number, array: any[]) => O,
thisArg?: any
) => ArrayUnlessVoid<O>;
const result = outerArrayMethod.call(
Array.from(Array(width).keys()),
(x: number) => arrayMethod.call(
Array.from(Array(height).keys()),
(y: number) => callback(x, y)
));
return result;
}
Blecch. It would be nice if verbGrid could take something that turns a callback-returning-T into an ArrayUnlessVoid<T> and returns an ArrayUnlessVoid<ArrayUnlessVoid<T>>. But the compiler can't really see that Array.prototype.map() matches the former definition. So I add a new type parameter O to take the place of ArrayUnlessVoid<T> and return ArrayUnlessVoid<O> instead.
It would also be nice if there were some easier way to describe the two different contexts in which you use arrayMethod in the implementation. Instead I just give up and use a type assertion to give the outer call to arrayMethod a different type signature.
Anyway, you can see that these work as advertised:
const createGrid = verbGrid(Array.prototype.map);
// const createGrid: <U>(
// width: number, height: number, callback: (x: number, y: number) => U
// ) => U[][]
const visitGridCoordinates = verbGrid(Array.prototype.forEach);
// const visitGridCoordinates: (
// width: number, height: number,
// callback: (x: number, y: number) => void
// ) => void
Okay, hope that helps; good luck!
Playground link to code

Related

Use Ramda's clone in pipe in a type-safe way

I'd like to use Ramda to clone and update objects in a type-safe way (inspired by this idiom) but I can't get it working in a type-safe way.
Updating a nested object works in a type-safe way perfectly fine:
interface Person {
name: string
department: {
name: string
budget: number
manager?: string
}
}
const personX: Person = {
name: 'Foo Bar',
department: {
name: 'x',
budget: 2000,
},
}
const addManager = (name: string): (input: Person) => Person => assocPath([
'department',
'manager',
], name)
const x = addManager('Michael Scott')(personX) // x is of type `Person`
I can also successfully combine functions using pipe or compose:
const addManager = (name: string): (input: Person) => Person => assocPath([
'department',
'manager',
], name)
const increaseBudget = (budget: number): (input: Person) => Person => assocPath([
'department',
'budget',
], budget)
const addManagerAndUpdateBudget = pipe(addManager('MichaelScott'), increaseBudget(10000))
const x = addManagerAndUpdateBudget(personX) // x is still of type Person
However, as soon as I use clone it fails:
const addManager = (name: string): (input: Person) => Person => assocPath([
'department',
'manager',
], name)
const increaseBudget = (budget: number): (input: Person) => Person => assocPath([
'department',
'budget',
], budget)
const addManagerAndUpdateBudget = pipe(clone, addManager('MichaelScott'), increaseBudget(10000))
const x = addManagerAndUpdateBudget(personX) // Person is not assignable to readonly unknown[]
Might this be an issue with the types? Or am I missing something here?
When using R.pipe (or R.compose) with other Ramda generic functions (such as R.clone) TS sometimes fails to infer the correct types, and the actual signature of the created function.
Note: I'm using Ramda - 0.28.0 and #types/ramda - 0.28.8.
In your case we want Ramda to use this signature - A list of arguments pass to the created function (TArgs), and then 3 return types of the piped functions (R1, R2, R3):
export function pipe<TArgs extends any[], R1, R2, R3>(
f1: (...args: TArgs) => R1,
f2: (a: R1) => R2,
f3: (a: R2) => R3,
): (...args: TArgs) => R3;
Since Ramda doesn't infer them, we'll need to add them explicitly (sandbox):
const addManagerAndUpdateBudget = pipe<[Person], Person, Person, Person>(
clone,
addManager('MichaelScott'),
increaseBudget(10000)
);
Arguments - a tuple with a single Person, and each return value is also a Person. We need to state all of them, so that TS would use the specific signature we need.
Another option is explicitly type the 1st function in the pipe, so TS can use it to infer the other types (sandbox):
const addManagerAndUpdateBudget = pipe(
clone as (person: Person) => Person,
addManager('MichaelScott'),
increaseBudget(10000)
);
(Disclaimer: I'm one of Ramda's core team.)
The Ramda team does not have a great deal of expertise in TypeScript. I've added the definitelytyped tag, as that project maintains the usual Ramda typings.
Not knowing TypeScript typings well, I don't understand why this doesn't work, as when I read the clone definition:
export function clone<T>(value: T): T;
export function clone<T>(value: readonly T[]): T[];
and the relevant pipe definition
export function pipe<TArgs extends any[], R1, R2, R3>(
f1: (...args: TArgs) => R1,
f2: (a: R1) => R2,
f3: (a: R2) => R3,
): (...args: TArgs) => R3;
everything looks right to me. I wonder if you need to declare the resulting of addManager and increaseBudget as Person. But that's just a guess, from a non-TS person.
I answered chiefly because I want to point out that for many uses, you will not need to use clone because assocPath already does the equivalent for any data it's altering.
const person2 = increaseBudget (10000) (person1)
person2 == person1 //=> false
person2 .department == person1 .department //=> false
Of course assocPath uses structural sharing where it can and does not do a full clone:
const person2 = assocPath ('startDate', '2014-07-12') (person1)
person2 .department == person1 .department //=> true
But for many uses, especially if further modifications are done using Ramda or with other immutable techniques, clone is simply unnecessary.
-- Scott

Error ts(2322) trying to define a generic function signature in Typescript

I'm trying to define an interface like this:
interface ColumnGenerator {
columnName: string;
columnValuesAsArgs?: string[];
generator: <T = any[], R = any>(...args: T extends any[] ? T : [T]) => R;
}
the value of generator should be a function that takes any number of arguments (if any) of any type, and then return something.
But, when I do something like:
const generatedColumn: ColumnGenerator = {
columnName: 'creation_date',
generator: () => new Date()
}
the compiler yields the next error:
Type 'Date' is not assignable to type 'R'.
'R' could be instantiated with an arbitrary type which could be unrelated to 'Date'.
I'm fairly new to Typescript, so I don't have an idea of what am I doing wrong here. Could anyone give me an explanation?
the value of generator should be a function that takes any number of arguments (if any) of any type, and then return something.
The type that you described here is (...args: any[]) => any. That type is a function that takes any number of arguments of any type & then returns anything.
type ColumnGenerator = {
columnName: string;
columnValuesAsArgs?: string[];
generator: (...args: any[]) => any;
}
const generatedColumn: ColumnGenerator = {
columnName: 'creation_date',
generator: () => new Date()
}
I'm not entirely sure what you're trying to do, but if you want to constrain things a bit, you could constrain make the ColumnGenerator type generic & configure that when it's used:
type ColumnGenerator<T = any> = {
columnName: string;
columnValuesAsArgs?: string[];
generator: (...args: any[]) => T;
}
const generatedColumn: ColumnGenerator<Date> = {
columnName: 'creation_date',
generator: () => new Date(), // this will now error if `Date` isn't returned from this function
}
The problem is you are trying to carry type information in a type that doesn't support it. In other words, the Interface or type has a static definition, and making an instance of it doesn't change its type.
Think of it this way, If I take in a ColumnGenerator into my function, what is the return type of the generator? There is no way to tell other than to widen it all the way out, and have it be any. As mentioned, you also could make the ColumnGenerator itself generic.
An even simpler way is to just say that the generator itself (any) => any, but then use generics later to be more specific. The key is that the generatedColumn is not typed as a ColumnGenerator. It is in fact a more specific type. This allows you to act on the information on the generic later on. Typescript uses structural or "duck" typing. That is, if it quacks like a duck, it is a duck. So if you leave your variables as the exact type they are, you have more information later to use to infer what they acually look like.
For example:
type ColumnGenerator = {
columnName: string;
columnValuesAsArgs?: string[];
generator: (...args: any[]) => any;
}
const generatedColumn = {
columnName: 'creation_date',
generator: () => new Date()
}
function test<TCol extends ColumnGenerator>(col: TCol): ReturnType<TCol["generator"]> {
return col.generator();
}
//Date
let t = test(generatedColumn);

Typescript - Nested arrow function typing

I have this code for deferring the execution of a function
export type DeferredFunction<T> = () => T | PromiseLike<T>;
export class Deferrable<T> {
protected df: DeferredFunction<T>;
constructor(df: DeferredFunction<T>) {
this.df = df;
}
public async execute(): Promise<T> {
return this.df();
}
}
export const defer = <T>(df: DeferredFunction<T>): Deferrable<T> => new Deferrable<T>(df);
That works fine and I can run code like
await defer(() => someFunction('foo', 'bar')).execute();
but I what I want to do is type DeferredFunction in a way that I can specify the inner function's signature but I can't get it working. In generic cases the above works but when I want to limit the arguments such that they are specific to a certain type of function I don't have that kind of control.
For clarity, I want to be able to type the inner function's inputs like (as an example)
export type InnerDeferredFunction<T> = (a: string, b: number, c: SomeObjectType) => T | PromiseLike<T>
Any help would be greatly appreciated!
What "inner function" are you talking about? Is it someFunction? If so then the type of DeferredFunction<T> has no handle on it, since it's a function called by the implementation of DeferredFunction<T>. There's no way in TypeScript to specify "a function whose implementation must call a function of type (x: string, y: number, z: boolean) => string". Implementation details are not part of a function's call signature.
The only way I can imagine to begin to approach this would be for DeferredFunction<T> to accept as a parameter the inner function you want to call, along with the list of arguments to call it with. This might not be what you're looking for, but it's the closest that the type system can represent.
Something like this:
export type InnerDeferredFunction<T, A extends any[]> = (...args: A) => T | PromiseLike<T>;
export type ZeroArgDeferredFunction<T> = InnerDeferredFunction<T, []>
Here I'm keeping A generic but you can specify it to some hardcoded list of arguments. I've renamed your DeferredFunction to ZeroArgDeferredFunction to be explicit that it doesn't need arguments.
But now Deferrable needs to know about T and A:
export class Deferrable<T, A extends any[]> {
protected df: ZeroArgDeferredFunction<T>;
constructor(df: InnerDeferredFunction<T, A>, ...args: A) {
this.df = () => df(...args);
}
public async execute(): Promise<T> {
return this.df();
}
}
And you can see that you have to construct one by passing it the inner function and its arguments, and the ZeroArgDeferredFunction is built inside the constructor and is not passed in.
There are different ways to define defer(). It could be a thin wrapper around new Deferrable the way you had it, or you could imagine splitting it up so that the args come first:
export const defer = <A extends any[]>(...args: A) => <T>(
df: InnerDeferredFunction<T, A>): Deferrable<T, A> => new Deferrable<T, A>(df, ...args);
And then you can test it like this:
function someFunction(x: string, y: string) {
return (x + y).length;
}
function anotherFunction(x: number, y: number) {
return (x * y).toFixed()
}
const deferFooBar = defer('foo', 'bar');
await deferFooBar(someFunction).execute(); // okay
await deferFooBar(anotherFunction); // error! string is not assignable to number
Once you call deferFooBar('foo', 'bar'), the returned value will only accept functions that can be safely called with the arguments foo and 'bar'. That means someFunction will be accepted and anotherFunction will be rejected.
Okay, hope that helps; good luck!
Playground link to code

Typescript: Evaluate type of generic function

Is there any trick to "evaluate" the type of a generic fuction?
Consider the following:
type Arr = <A>() => A[]
type Ev<G, A> = ???
Question: Is it possible to fill in ??? such that Ev<Arr, A> equals () => A[]? (As compared to <A>() => A[])
(Update 2022/04/26)
Something similar/related will be possible with TS 4.7; Even though that's not fully what we'd want here.
https://github.com/microsoft/TypeScript/pull/47607
Some more examples for the desired behavior:
Ev<<A>() => A[], number>
// should evaluate to
// () => number[]
Ev<<A>() => string, number>
// should evaluate to
// () => string
Ev<<A>() => [string, A], { some: "thing" }>
// should evaluate to
// () => [string, { some: "thing" }]
A simplified version of the question would be: Can we define
type EvNum<A> = ???
such that
EvNum<
<X>() => X
> // should be `number`
EvNum<
<X>() => X[]
> // should be `number[]`
EvNum<
<X>() => [X, "hi"]
> // should be `[number, "hi"]`
EvNum<
<X>() => SomeGenericType<X>
> // should be `SomeGenericType<number>`
EvNum<
<X>() => "constant"
> // should be `"constant"`
As to your first question, those are referred to as Higher Kinded Types, and are not supported in Typescript as of this answer.
A Higher Kinded Type is simply "A type that abstracts over some type that, in turn, abstracts over another type." So if you want a type, that you pass in a type to abstractly create a new type, that is an example of a higher kinded type. And it is not possible in TS.
You cannot pass a second type into a generic type and come out with a derived type.
Your last example (simplified) is literally ReturnType so not sure what you are meaning. It is perfectly possible to come up with. But you can't make a type that comes up with it.
type EvNum<T> = () => T;
type Arr<T> = T[];
function func<T>(param: EvNum<T>): T {
return param();
}
let x1 = func(()=> 4); //number
let x2 = func(()=> [4]); //number[]
let x3 = func(()=> [4, "hi"] as const); //[4, "hi"]
let x4 = func(()=> "constant" as const); //"constant"
let cool: Arr<number> = [4, 5, 6];
let x5 = func(() => cool); //Arr<number>
This passes your requested types
If I understand you correctly it should be:
type EV<T> = () => T;
Otherwise the question makes no sense or must be explained in more detail.

Define the type of an object with string keys and function values

I have a node JS function with one parameter that is an object with keys and the values are functions that then resolve to the underlying values.
We have just switched over to TS and I don't know how to define the key:value types of a parameter and further I don't know how to define a function as the value type?
The TS function looks like this...
const myJSFunction = o => input => ...
Where o is the string:function object. And input is then passed into each of the function values of o.
So I was thinking of having some signature along the lines of...
// define the generic <R> function as <U => any>
// define the generic <T> as an object of { string : R }
const myTSFunction = (o: T) => (input: U) => ...
Or something? I'm clutching at straws here as I don't know Typescript well enough to know what is possible with generics.
Thanks
What about something like this :
// We define what the type o is
// key: string means "any key should ..."
interface Obj<T> {
[key: string]: (input: T) => void,
};
// We instantiate an object for the test
const o: Obj<string> = {
a: (input) => { },
b: (input) => { },
};
// We define the function to work with any type of value of obj
// and call it for the test
function myTSFunction<T>(obj: Obj<T>, val: T): void {
obj[0](val);
}
Grégory NEUT answer helped a lot but there were some other constraints that I discovered on the way (that JS had been hiding away). I'm using Lodash so my object was not just an object but a type that they had defined.
So I defined some new types...
type TransformerFunction<T> = (o: T) => any;
type TransformerObject<T> = Dictionary<TransformerFunction<T>>;
And then the function became like...
export const objectTransform = <T extends any>(o: TransformerObject<T>) => <U extends T>(json: U): Dictionary<any> => _.flow(
_.mapValues((f: TransformerFunction<T>) => f(json)),
_.omitBy(_.isUndefined),
_.omit('undefined'),
)(o);
This is how I have been transforming JSON in JS and now moving it over to TS and loving the generics.

Resources