I want to write a call function:
function call<F extends (...arg: any) => any, P extends Parameters<F>>(
fn?: F,
...arg: P
): ReturnType<F> | undefined {
if (fn) return fn(...arg) // Type 'P' must have a '[Symbol.iterator]()' method that returns an iterator
}
const fn = (a: string) => {}
// automatically infer `fn` arguments type
call(fn, )
what should I do?
This is a known bug in TypeScript; see microsoft/TypeScript#36874. It looks like the problem is triggered by F extends (...arg: any) => any instead of F extends (...arg: any[]) => any. That is, your arg rest parameter is of the anything-at-all any type instead of the array-of-anything any[] type. The obvious workaround is therefore to change any to any[], which shouldn't be a problem because function rest parameters are essentially always arraylike:
function call<F extends (...arg: any[]) => any, P extends Parameters<F>>(
fn?: F, ...arg: P): ReturnType<F> | undefined {
return fn?.(...arg) // okay (I used optional chaining here to deal with missing fn)
}
Looks good!
That's the answer to the question as asked, although it is useful to note that generally speaking, the preferred way to do this is to make call() generic in P (the rest parameter type) and R (the return type), and not use the Parameters<T> and the ReturnType<T> utility types:
function call<P extends any[], R>(fn?: (...arg: P) => R, ...arg: P): R | undefined {
return fn?.(...arg); // okay
}
They are similar, but your approach ends up being unable to properly represent what happens when fn is itself generic:
const pair = <T, U>(left: T, right: U): [T, U] => [left, right];
const ret = call(pair, "abc", 123); // old version of call
// const ret: [any, any] | undefined ☹
whereas the approach I recommend can take advantage of the support for higher order type inference from generic functions:
const ret = call(pair, "abc", 123); // new version of call
// const ret: [string, number] | undefined 👍
If you can rewrite generics using conditional types like Parameters<T> and ReturnType<T>, it will sometimes improve the compiler's ability to produce desirable results.
Playground link to code
This is a tricky one, but the solution is not.
You have to ensure the typescript that args is iterable. You can do that like manually saying to him - Hey this will be an array, believe me
Working playground
function call<F extends (...arg: any) => any, P extends Parameters<F>>(
fn?: F,
...args: P
): ReturnType<F> | undefined {
if (fn){
// here is the ugly hint to typescript
return fn(...(args as any[]))
}
return undefined;
}
Have you considered using apply as a workaround?
function call<F extends (...arg: any) => any, P extends Parameters<F>>(
fn?: F,
...arg: P,
): ReturnType<F> | undefined {
if (fn) return fn.apply(undefined, arg);
return undefined;
}
Playground
Related
I want to write a function that modifies a method on a function prototype, like this:
function inject<
T,
O = {
[K in keyof T as T[K] extends (...args: any) => any ? K : never]: T[K];
},
K extends keyof O = keyof O,
F extends (...args: any) => any = O[K] // error
>(o: { prototype: T }, func_name: K, func: (ret: ReturnType<F>) => void) {}
But typescript reported an error saying type "O[K]" does not satisfy the constraint "(...args: any) => any".
How do I fix this, or should I write my code differently?
I don't know if this is the best or easiest way to do it, but I suppressed the error by changing the O generic.
function inject<
T,
O extends Record<any, (...args: any) => any> = {
[K in keyof T as T[K] extends (...args: any) => any ? K : never]: T[K] extends (...args: any) => any ? T[K] : never;
},
K extends keyof O = keyof O,
F extends (...args: any) => any = O[K]
>(o: { prototype: T }, func_name: K, func: (ret: ReturnType<F>) => void) {}
Part of the problem is that O could be an arbitrary value, so typescript has no reason to believe that O[K] will be a function. To fix this, O should extend an object with function values (just like K extends keyof O and is assigned to keyof O). The T[K] extends (...args: any) => any ? T[K] : never is necessary because I think typescript isn't smart enough to infer that the value has to be a function due to the never guard in the value. It needs to know that it's a function so it can fit the Record<any, (...args: any) => any> signature.
Your main problem is that your O type parameter has a default type argument but is not constrained to that type, and in any case, O does not appear anywhere in the types of your function parameters, so there is no inference site for it.
Ideally you want as few type parameters as possible and you want them to appear in your function parameters as directly as possible. If you have a type parameter X then it is most easily inferred from a parameter of type X (e.g., <X,>(x: X) => void); it can also be inferred from a parameter with a property of type X (e.g., <X,>(v: {v: X}) => void, or from a homomorphic mapped type on X (e.g., <X,>(v: {[K in keyof X]: ...X[K]...}) => void, see What does "homomorphic mapped type" mean? for more info). And more complicated things can also sometimes be used in inference, but the general rule is you want it to appear as directly as possible.
So I'd probably write inject()'s call signature as follows:
function inject<
K extends PropertyKey,
T extends Record<K, (...args: any) => any>
>(
o: { prototype: T },
func_name: K,
func: (ret: ReturnType<T[K]>) => void
) { }
Here K is the type of func_name, and can be inferred to be the string literal type of the func_name argument because it is constrained to PropertyKey which is string | number | symbol. And T is the type of the prototype property of o , where T is constrained to have a function property at key K (using the Record<K, V> utility type). So T will be inferred from the prototype property of the o argument, and there will be an error if o.prototype[func_name] isn't of a function type.
Finally, the type of func is a callback whose argument is ReturnType<T[K]> using the ReturnType<T> utility type. This will not be used to infer T or K (it's too complicated)... but that's okay because T and K should already be inferred from other arguments. Instead, the compiler will use ReturnType<T[K]> with the already-inferred T and K to contextually type the callback argument. That is, inference is going the other way here. Let's test it out:
class Foo {
bar() {
return 123;
}
baz() {
return "xyz"
}
qux = 10
}
inject(Foo, "bar", x => x / 2);
inject(Foo, "bar", x => x.toUpperCase()); // error!
// Property 'toUpperCase' does not exist on type 'number'
inject(Foo, "baz", x => x.toUpperCase());
inject(Foo, "qux", x => x) // error!
// Type 'number' is not assignable to type '(...args: any) => any'
Looks good. The compiler is happy about the first call, and it understands that x is of type number because Foo.prototype.bar is a number-returning function. The second call it complains because number doesn't have a toUpperCase. Then the third call is okay because new Foo().baz() returns a string. The fourth call fails because new Foo().qux isn't a function at all; it's a number (plus it won't be on the prototype at all, but TypeScript models prototypes as identical to class instances, for better or worse, so that's not something the compiler could catch).
Playground link to code
I am trying to write a function that accepts an API interface (I've created a sample here), and wraps a Proxy around it, so that any calls to the that API's methods get intercepted, and I can do some logging, custom error handling etc. I am having a terrible time with the types. This is similar to another question I have asked (Writing wrapper to third party class methods in TS), but uses a completely different approach than that one, based on some feedback I got.
Currently I am getting
Element implicitly has an 'any' type because expression of type 'string | symbol' can't be used to index type 'API'. No index signature with a parameter of type 'string' was found on type 'API'. which makes sense given that sayHello is not strictly a string as far as typescript is concerned, but I do not know the best way to be able to get methods on this class without uses the property accessor notation.
class API {
sayHello(name: string) {
console.log(“hello “ + name)
return name
}
}
export default <T extends API>(
api: T,
) =>
new Proxy(api, {
get(target, prop) {
if (typeof target[prop] !== "function") { // type error here with "prop"
return target[prop]; // and here
}
return async (...args: Parameters<typeof target[prop]>) => {
try {
const res = await target[prop](...args); // and here
// do stuff
return res
} catch (e) {
// do other stuff
}
};
},
});
Is this possible in TS?
TypeScript doesn't currently model the situation when a Proxy differs in type from its target object. There's a longstanding open feature request for this at microsoft/TypeScript#20846, and I don't know if or when the situation will change.
For now if you want to do this you'll need to manually describe the expected return type of your proxy function, and use type assertions liberally inside the implementation in order to suppress any errors. This means you'll need to verify that your function is type safe yourself; the compiler won't be able to help much.
Here's one possible approach:
const prox = <T extends API>(api: T) => new Proxy(api, {
get(target: any, prop: string) {
if (typeof target[prop] !== "function") {
return target[prop];
}
return async (...args: any) => {
try {
const res = await target[prop](...args);
return res;
} catch (e) { }
};
},
}) as any as { [K in keyof T]: AsyncifyFunction<T[K]> };
type AsyncifyFunction<T> = T extends (...args: infer A) => infer R ?
(...args: A) => Promise<Awaited<R>> : T;
The idea is that prox(api) returns a version of api where every non-function property is the same, but every function property has been changed to an async version that returns a Promise. So if api is of type T, then prox(api) is of mapped type { [K in keyof T]: AsyncifyFunction<T[K]> }, where AsyncifyFunction<T> is a conditional utility type that represents the transformation of each property type. If X is a function type with argument tuple A and return type R, then AsyncifyFunction<X> is (...args: A) => Promise<Awaited<R>>, using the Awaited<T> utility type to deal with any nested promises (we don't want a Promise<Promise<number>> to come out of this, for example).
Okay, let's test it:
class Foo extends API {
str = "abc";
double(x: number) { return x * 2 };
promise() { return Promise.resolve(10) };
}
const y = prox(new Foo());
/* const y: {
str: string;
double: (x: number) => Promise<number>;
promise: () => Promise<number>;
sayHello: (name: string) => Promise<void>;
} */
So, according to the compiler, y has a string-valued str property, and the rest of its properties are asynchronous methods that return promises. Notice that the promise() method returns Promise<number> and not Promise<Promise<number>>.
Let's make sure it works as expected:
console.log(y.str.toUpperCase()) // "ABC"
y.sayHello("abc") // "helloabc"
y.double(123).then(s => console.log(s.toFixed(2))) // "246.00"
y.promise().then(s => console.log(s.toFixed(2))) // "10.00"
Looks good!
Playground link to code
You can try add index signature to your class:
class API {
/* -- index signature -- */
[index:string|symbol]: any;
sayHello(name: string) {
console.log("hello" + name)
}
}
export default <T extends API>(
api: T,
) =>
new Proxy(api, {
get(target, prop) {
if (typeof target[prop] !== "function") {
return target[prop];
}
/* -- had to store it in a variable -- */
const data = target[prop];
return async (...args: Parameters<typeof data>) => {
try {
const res = await target[prop](...args);
// do stuff
return res.data;
} catch (e) {
// do other stuff
}
};
},
});
I'm trying to create a React higher-order component for a specific use-case, the problem is boiling down to the following:
function sample<TObj, P extends keyof TObj, F extends keyof TObj>(
obj: TObj,
prop: P,
setProp: TObj[F] extends (value: TObj[P]) => void ? F : never
) {
obj[setProp](obj[prop]);
}
I want to be able to pass an object, a string which should be a key of that object, and another key of that object but which is required to be a function.
This can be simplified further as such:
function sample2<TObj, F extends keyof TObj>(
obj: TObj,
setProp: TObj[F] extends () => void ? F : never
) {
obj[setProp]();
}
It seems to me that because I use the conditional type, it can be guaranteed that obj[setProp] will be a function but I get the error:
This expression is not callable.
Type 'unknown' has no call signatures.ts(2349)
As can be seen below, the function will error if it will be called with a key that doesn't respect the requirement. But that same requirement doesn't seem to be applied inside the function.
I understand that this could be seen as a XY problem, but it got me really interested in whether there is a way to make this specific problem work correctly.
Inside the implementation of sample2(), the type TObj[F] extends () => void ? F : never is an unresolved conditional type. That is, it's a conditional type that depends on a currently-unspecified generic type parameter to be resolved. In such cases, the compiler generally doesn't know what to do with it and treats it as essentially opaque. (See microsoft/TypeScript#23132 for some discussion of this.) In particular it doesn't realize that TObj[Tobj[F] extends ()=>void ? F : never] will ultimately have to resolve to some subtype of ()=>void.
In general I'd avoid conditional types entirely unless they are necessary. The compiler can more easily understand and infer from mapped types like Record<K, V>:
function sample2<K extends PropertyKey, T extends Record<K, () => void>>(
obj: T,
prop: K
) {
obj[prop]();
}
And that behaves similarly when you call it:
const obj2 = {
func() { console.log("func") },
prop: 42
};
sample2(obj2, "func"); // okay,
//sample2(obj, "prop"); // error
// ~~~ <-- number is not assignable to ()=>void
EDIT: to address the original sample(), I'd use this definition:
function sample<
PK extends PropertyKey,
FK extends PropertyKey,
T extends Record<PK, any> & Record<FK, (v: T[PK]) => void>
>(
comp: T,
prop: PK,
setProp: FK
) {
comp[setProp](comp[prop]);
}
const obj = {
func(z: number) { console.log("called with " + z) },
prop: 42
}
which, I think, also behaves how you'd like:
sample(obj, "prop", "func"); // called with 42
sample(obj, "prop", "prop"); // error!
// ~~~ <-- number not assignable to (v: number)=>void
sample(obj, "func", "func"); // error!
// ~~~ <-- (v: number)=>void not assignable to number
Okay, hope that helps; good luck!
Link to code
Or how do you define multiple signatures for a returned function
I'm trying to make a curried function but I'm having trouble with the definition overloads. In specific if you call parallelMap with one argument, you can call the next argument with 1 or 2 arguments. However the def is marked as invalid.
[ts] Overload signature is not compatible with function implementation. [2394]
export function parallelMap<T, R> (concurrency: number): (func: (data: T) => R | Promise<R>) => (iterable: AnyIterable<T>) => AsyncIterableIterator<R>
Full implementation;
export function parallelMap<T, R> (concurrency: number): (func: (data: T) => R | Promise<R>, iterable: AnyIterable<T>) => AsyncIterableIterator<R>
export function parallelMap<T, R> (concurrency: number): (func: (data: T) => R | Promise<R>) => (iterable: AnyIterable<T>) => AsyncIterableIterator<R>
export function parallelMap<T, R> (concurrency: number, func: (data: T) => R | Promise<R>): (iterable: AnyIterable<T>) => AsyncIterableIterator<R>
export function parallelMap<T, R> (concurrency: number, func: (data: T) => R | Promise<R>, iterable: AnyIterable<T>): AsyncIterableIterator<R>
export function parallelMap<T, R> (
concurrency: number,
func?: (data: T) => R | Promise<R>,
iterable?: AnyIterable<T>,
) {
if (func === undefined) {
return <A, B>(curriedFunc: (data: A) => B | Promise<B>, curriedIterable?: AnyIterable<A>) => parallelMap(concurrency, curriedFunc, curriedIterable)
}
if (iterable === undefined) {
return (curriedIterable: AnyIterable<T>) => parallelMap<T, R>(concurrency, func, curriedIterable)
}
return _parallelMap<T, R>(concurrency, func, iterable)
}
Thanks!
Overloads are useful when different parameter types should result in different return types. It's not useful to have two different overload signatures with the same parameter types. That's because, as the handbook says:
[The compiler] looks at the overload list, and proceeding with the first overload attempts to call the function with the provided parameters. If it finds a match, it picks this overload as the correct overload.
Your first two overloads have the same parameter types, so the second overload will never be used. That means if you call parallelMap() with one argument, it will return a two-argument function and that's it. It does not return a one-argument function.
Let's remedy that. The solution here is when you call parallelMap() with one argument, you want to return an overloaded function, instead of a function of just one argument or just two arguments.
Additionally, you want the generic type parameters to be on that returned function, since when you call parallelMap(concurrency) you don't know at that point what T and R will end up being.
So replace those first two signatures with this:
export function parallelMap(concurrency: number): {
<T,R>(func: (data: T) => R | Promise<R>, iterable: AnyIterable<T>): AsyncIterableIterator<R>,
<T,R>(func: (data: T) => R | Promise<R>): (iterable: AnyIterable<T>) => AsyncIterableIterator<R>
}
Now that says "if you call parallelMap() with one argument, it will return another function which can be called with two arguments of type XXX and YYY and return ZZZ, and it can also be called with one argument of type XXX and return a function from YYY to ZZZ."
Now it should mostly work. Notice that because you are using overloads, the following code isn't exactly correct:
if (func === undefined) {
return <A, B>(
curriedFunc: (data: A) => B | Promise<B>,
curriedIterable?: AnyIterable<A>
) => parallelMap(concurrency, curriedFunc, curriedIterable) // error!
}
After all, none of the overload call signatures accept a possibly undefined third argument. You either call it with two or three defined arguments. So you should change that to something like:
if (func === undefined) {
return <A, B>(
curriedFunc: (data: A) => B | Promise<B>,
curriedIterable?: AnyIterable<A>
) => curriedIterable ?
parallelMap(concurrency, curriedFunc, curriedIterable) :
parallelMap(concurrency, curriedFunc)
}
which calls two different overloads of parallelMap() depending on whether curriedIterable is defined.
Okay, hope that helps. Good luck!
Would like to seek help writing d.ts file for https://github.com/paldepind/union-type
With the union type below,
let Maybe = Type({
Nothing: []
, Just: [Number]
})
I would love to see compiler error in case Maybe.Nothing() is mistyped as Maybe.None()
I tried to capture the keys from the object literal, but the compiler still fails to recognize Nothing and Just in the resulting type Maybe.
interface Obj {
prototype: any
case: (x: {[index: string]: (...args) => any}) => any
caseOn: (x: {[index: string]: (...args) => any}) => any
}
interface Union<T> {
(desc: T): T & Obj
}
var Type: Union<{[key: string]: any}>
export = Type
I think what you are looking for are Index Type, see the Advanced Types section of the handbook for more explanation on how they work.
I've also been making a declaration file for paldepind/union-type, but it's incomplete :
declare module 'union-type' {
type Constructors =
| StringConstructor
| NumberConstructor
| ArrayConstructor
| BooleanConstructor
| ObjectConstructor
| FunctionConstructor
export default function Type<
T extends {
[k: string]: (Constructors | ((arg?: any) => boolean | Constructors))[]
},
K extends keyof T
>(
desc: T
): {
// case(cases: { [k in K]: (...args: any[]) => void }, obj: any): void
[k in K]: (...args: any[]) => any
}
}
I'm also trying to find a workaround that library in TS...
If you are looking for a Maybe implementation here is one I wrote once upon a time
/**
* A Maybe implementation
* that is JavaScript first
* i.e. simple typed abstraction over null/undefined
*/
export class Maybe<T>{
private _value: T;
/** Based on value it will be Some or None */
constructor(value: T) {
this._value = value;
}
/** Shorthand for constructor */
static Some<T>(value: T): Maybe<T> {
if (value === null || value === undefined) {
throw new Error('value for some cannot be null or undefied');
}
return new Maybe(value);
};
static None<T>(): Maybe<T> {
return new Maybe(null);
};
get value(): T {
return this._value;
}
get isSome() {
return this._value !== null && this._value !== undefined;
}
get isNone() {
return !this.isSome;
}
map<U>(mapper: (now: T) => U): Maybe<U> {
if (this.isSome) {
return new Maybe(mapper(this._value));
}
else {
return new Maybe(null);
}
}
}
That said, I find it pretty useless, much simpler to be just aware of null/undefined and use valid property on your objects (more https://medium.com/#basarat/null-vs-undefined-in-typescript-land-dc0c7a5f240a)
More
That all said typescript is going to get first class nullability support https://github.com/Microsoft/TypeScript/pull/7140 and you will be able to do number | null | undefined and you would not be allowed to assign null to a number i.e. let foo:number = null; // Error foo is not nullable