Typescript - Nested arrow function typing - node.js

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

Related

Typescript: get parameters type of a method in a composition class

Hello there fellow TS community. So here's a problem: imagine we have a composition interface like this:
type IWorker = {
serviceTask: IServiceTask,
serviceSomethingElse: IServiceColorPicker
}
type IServiceTask = {
doSomething: (arg1: number) => Promise<void>;
}
type IServiceSomethingElse = {
doSomething1: (params: Record<string, any>) => Promise<string>;
}
We also have implementation for these interfaces, but I just skipped them here. Lemme now if you need those!
Anyways so let's say we have a function that runs one of the methods in a nodejs worker thread:
const runInWorker<T extends keyof IWorker, K extends keyof IWorker[T], P extends ????>(parameters: {workerService: T, serviceMethod: K, methodParams: P}) => {
const worker: IWorker = new Worker();
worker[workerService][serviceMethod].call(this, methodParams)
...
}
This runInWorker function has three arguments:
Name of service we're about to run - workerService: serviceTask or serviceSomethingElse
Name of method of a service - serviceMethod: doSomething or doSomething1
Parameters to pass into that method. methodParams: number | Record<string, any>. I've tried the Parameters<IWorker[T][K]> generic but it doesn't resolve to parameters - says 'type IWorker[T][K] doesn not satisfy constraint (...args: any) => any'- which is Parameters generic's type argument constraints
So I have 2 questions:
How to get rid of "call doesn't exist on type IWorker[T][K]" TS error?
How do I infer argument types of my methods?
Thanks!
With question 1 I've expected methods to be callable so I've tried just calling them with
worker[workerService][serviceMethod] but of course it didn't help
With question 2 as I said I've tried to use Parameters generic but looks like it's no good with other generics like that - it just doesn't know those types yet
Typescript cannot see that all fields on IWorker needs to be methods. You can either have some constraint on the level of type signature, or you can make a kind of constraint by having conditional type. The latter can be implemented like this. Swap the Parameters type with
type GetParameters<T> = T extends (...args: any) => any ? Parameters<T> : never
Here you check whether T is actually a function. If you would declare a non-function field on IWorker, then the third parameter would have never type, that is a type for you can't have any value, that means you couldn't make a call to such a function that would typecheck.
Thus, by having this signature, you can make the calls:
function runInWorker2<T extends keyof IWorker, K extends keyof IWorker[T]>(parameters: {workerService: T, serviceMethod: K, methodParams: GetParameters<IWorker[T][K]>}) {
// implementation
}
const t = runInWorker2({workerService: "serviceTask", serviceMethod: "doSomething", methodParams: [3]})
const t2 = runInWorker2({workerService: "serviceSomethingElse", serviceMethod: "doSomething1", methodParams: [{x: "y"}]})

Typescript extending a generic type

I have the following generic interface in my typescript code:
interface BaseResponse<T> {
status_code: string;
data: T;
}
I thought I would be able to use that base interface, without specifying the base's type parameter, in a generic function like this:
class MyService {
static async post<T extends BaseResponse>(path: string, data: any): Promise<T> {
// implementation here
}
}
But this gives the following error:
Generic type 'BaseResponse<T>' requires 1 type argument(s).(2314)
I can fix this error by updating the code like so:
class MyService {
static async post<T extends BaseResponse<U>, U>(path: string, data: any): Promise<T> {
// implementation here
}
}
But this requires me to pass two type parameters when I call the function as below. I was hoping I could only pass one and it could infer the second, but that gives me the error Expected 2 type arguments, but got 1.(2558). Is there any way to accomplish this?
// What I want to be able to do (Causes error mentioned above):
const response1 = await MyService.post<CustomerResponse>('/customers', postData);
// What I have to do instead (note the two type parameters)
const response2 = await MyService.post<CustomerResponse, CustomerData>('/customers', postData);

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);

How to implement a type safe, phantom types based builder in typescript?

The idea is to allow a call to the .build() method only upon having all the mandatory parameters filled. So the constructor should be taught to do some validation.
If I understand you correctly, you have some kind of builder class, which by default doesn't have all the required parameters. And that class has a method, which updates its state. Only when all required parameters are set, only then build method should be available.
So first of all, we have T type which partial (all properties are optional).
On each update, we should return a new instance with type T & Record<K, T[K]> - it means optional T + one non-optional property.
Finally, we can use conditional types to allow build only when T extends Required<T>.
So the final solution:
function createBuilder<T>(initialData: T) {
return {
update: <K extends keyof T>(key: K, value: T[K]) => {
return createBuilder<T & Record<K, T[K]>>({
...initialData,
[key]: value
})
},
build: (() => {
//
}) as T extends Required<T> ? () => boolean : undefined
}
}
const builder1 = createBuilder<Partial<{
key1: string,
key2: number
}>>({})
builder1.build()
// Cannot invoke an object which is possibly 'undefined'
const builder2 = builder1.update('key1', 'abc')
builder2.build()
// Cannot invoke an object which is possibly 'undefined'
const builder3 = builder2.update('key2', 10)
builder3.build()
// No error
Hovewer, there is no point having this logic. If you can statically update the object, you probably can set all properties in the constructor.

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