Wrapping class in Proxy object TS - javascript

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

Related

Using Parameters<F> infer arguments type

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

args unable to be passed into wrapped function

I have several modules (or bridges) that follow the same format as below:
export interface Bridge {
foo: (a: string, b: boolean, c: string) => number;
bar: (a: number, b: number, c: string, d: string) => string;
x: (a: string) => boolean;
y: () => null;
}
As a result, I need to create a layer than does some if-else processing to import the respective module. However, the functions within these modules will require a fallback mechanism that is made to be generic.
export type BridgeFunctions = keyof Bridge;
/* Called when a particular bridge throws an error */
export async function fallback(fnName: BridgeFunctions, ...args: any[]): Promise<any> {
const fallbackBridge = await import('./fallbackBridge');
const fn = fallbackBridge[fnName];
return fn(...args);
}
However, VSCode returns me this error:
A spread argument must either have a tuple type or be passed to a rest parameter.ts(2556)
Tried using call and apply, which led to this error instead:
The 'this' context of type ... is not assignable to method's 'this' of type '(this: any[]) => Promise<any>'.
Based on my understanding, in vanilla Javascript, passing args straight into fn should work. How do I emulate the same in TypeScript?
EDIT:
The Bridge modules are used as follows:
function getBridge(): Bridge {
if (inIOS()) {
return await import('./ios');
}
// ...
return await import ('./web');
}
export function foo(a: string, b: boolean, c: string): number {
try {
const bridge = await getBridge();
return bridge.foo(a, b, c);
} catch (error: any) {
// ...
if (error.bridge.failed) {
return await fallback('foo', a, b, c);
}
}
}
This is how I would do it.
Basically, you want to make sure the user passes valid arguments to your function. To do this, you set the args to be the parameters of the passed in function.
export async function fallback<FN extends keyof Bridge>(fnName: FN, args: Parameters<Bridge[FN]>){
Then, to bypass the error, just type fn to any:
const fn = fallbackBridge[fnName] as any;
return fn(...args);
Using any here is fine since you already know the types are compatible.
The problem here is that your fallbackBridge functions require a certain number of parameters but you give them the type " any[] " that could have any length.
You could bypass this error with return fn(...(args as Parameters<typeof fn>));

Typescript dynamic function by key/value of object

Is there a way to set up a function based on an initial interface? I have a bunch of listOfFunctions and a an equal amount of interfaces that contain types of the functions. I would like to create a factory that returns a custom hook. The idea is to save a ton of code and make editing and adding features to all the hooks a breeze.
FunctionInterface Is all of the function names and their returns.
listOfFunctions is a setup function (the real code has a config) that returns a object with a list of functions.
theChosenFunction is a function that will be returned in the hook that allows me to interact with one of the listOfFunction functions. This means parameters (if needed). The functions in the real app are Promises that will set state of the hook. In this example, I just return a value and setState.
A) Is this possible to do with funcObject[funcName]() and create a function to invoke with typescript? This would need that ability to add params or not.
B) Is there a way to get the return value of a function type? So in this example:
type CanThisWork = () => string; I want to extract string; The idea is not to do a refactor on all of the listOfFunctions and FunctionInterfaces. There are a lot of them. If I have to create one complicated Factory, then I find this more economical.
interface FunctionInterface {
noParamsNoReturn: () => void;
noParamsNumberReturn: () => number;
propsAndNoReturn: (id: string) => void;
propsAndReturn: (id: string) => string;
}
const listOfFunctions = (): FunctionInterface => {
return {
noParamsNoReturn: () => void,
noParamsNumberReturn: () => 1,
propsAndNoReturn: (id: string) => console.log(id),
propsAndReturn: (id: string) => id,
}
}
function runThis<T>(funcName: keyof T, funcObject: T): void {
const [value, setValue] = React.useState();
// Can I have typescript set up the params here?
const theChosenFunction = ();
const theChosenFunction = (props) => {
// Can I have typescript invoke the function properly here?
const result = funcObject[funcName](); // funcObject[funcName](props)
if (result) {
setValue(result);
}
}
return {
theChosenFunction
}
}
const result = runThis<FunctionInterface>('noParamsNoReturn', listOfFunctions());
result.theChosenFunction()
// OR
result.theChosenFunction('someId');
There are built-in Parameters<T> and ReturnType<T> utility types that take a function type T and use conditional type inference to extract the tuple of parameters and the return type, respectively:
type SomeFuncType = (x: string, y: number) => boolean;
type SomeFuncParams = Parameters<SomeFuncType>; // [x: string, y: number]
type SomeFuncRet = ReturnType<SomeFuncType>; // boolean
As for your runThis() function,
I'd be inclined to give it the following typings and implementation, assuming you want the minimal changes that compile and run:
function runThis<K extends PropertyKey, F extends Record<K, (...args: any[]) => any>>(
funcName: K, funcObject: F
) {
const [value, setValue] = React.useState();
const theChosenFunction = (...props: Parameters<F[K]>): void => {
const result = funcObject[funcName](...props);
if (result) {
setValue(result);
}
}
return {
theChosenFunction
}
}
I've given the function two generic type parameters: K, corresponding to the type of funcName, and F, corresponding to the type of funcObject. The constraints K extends PropertyKey and F extends Record<K, (...args: any[])=>any> guarantee that funcName will be of a key-like type (string, number, or symbol), and that funcObject will have a function-valued property at that key.
Then, we can make theChosenFunction use spread and rest syntax to allow the function to be called with a variadic list of parameters. So (...props) => funcObject[funcName](...props) will accept any number of parameters (including zero) and pass them to the called function. TypeScript represents such lists-of-parameters as a tuple type. Therefore, having theChosenFunction's call signature look like (...props: Parameters<F[K]>) => void means that it will accept the same parameters as the entry in funcObject at the key funcName, and that it will not output anything (because your implementation doesn't output anything).
Let's see if it works:
const result = runThis('noParamsNoReturn', listOfFunctions());
result.theChosenFunction(); // okay
result.theChosenFunction('someId'); // error! Expected 0 arguments, but got 1.
const anotherResult = runThis('propsAndReturn', listOfFunctions());
anotherResult.theChosenFunction(); // error! Expected 1 arguments, but got 0.
anotherResult.theChosenFunction("someId"); // okay
runThis(listOfFunctions(), 'someId'); // error! '
// FunctionInterface' is not assignable to 'string | number | symbol'.
runThis('foo', listOfFunctions()); // error!
// Property 'foo' is missing in type 'FunctionInterface'
runThis(
'bar',
{ bar: (x: string, y: number, z: boolean) => { } }
).theChosenFunction("hey", 123, true); // okay
Looks good to me.
Playground link to code

Implementing safe navigation in typescript

Recently I came across the following article in Medium about Using ES6's Proxy for safe Object property access from Gidi Meir Morris. I really liked it and wanted to give it a try in my Typescript project for optional nested objects without loosing type checking.
In order to turn optional nested objects into all-required, I'm using the following type:
export type DeepRequired<T> = {
[P in keyof T]-?: DeepRequired<T[P]>;
};
Gidi's code in typescript (including some hacks...):
export interface Dictionary {
[key: string]: any;
};
const isObject = (obj: any) => obj && typeof obj === 'object';
const hasKey = (obj: object, key: string) => key in obj;
const Undefined: object = new Proxy({}, {
get: function (target, name) {
return Undefined;
}
});
export const either = (val: any, fallback: any) => (val === Undefined ? fallback : val);
export function safe<T extends Dictionary>(obj: T): DeepRequired<T> {
return new Proxy(obj, {
get: function(target, name){
return hasKey(target, name as string) ?
(isObject(target[name]) ? safe(target[name]) : target[name]) : Undefined;
}
}) as DeepRequired<T>;
}
Usage example:
interface A {
a?: {
b?: {
c?: {
d?: string
}
}
},
b: boolean,
c?: {
d: {
e: number
}
},
d?: Array<{e: boolean}>
}
const obj: A = {b: false};
const saferObj = safe(obj);
Scenarios that it works without TS errors:
test('should work for nested optional objects', () => {
expect(either(saferObj.a.b.c.d, null)).toEqual(null);
expect(either(saferObj.a.b.c.d, undefined)).toEqual(undefined);
expect(either(saferObj.a.b.c.d, 322)).toEqual(322);
});
test('should work for required members', () => {
expect(either(saferObj.b, null)).toEqual(false);
});
test('should work for mixed optional/required tree', () => {
expect(either(saferObj.c.d.e, null)).toEqual(null);
});
As for arrays...
test('should work for arrays', () => {
expect(either(saferObj.d[0].e, null)).toEqual(null);
});
TS compiler throws the following error:
[ts] Element implicitly has an 'any' type because type 'DeepRequired<{ e: boolean; }[]>' has no index signature.
Any idea how can i make this work for Arrays?
Your code will work as is on Typescript 2.9 and above because in Typescript 2.9 the keyof operator includes numeric and symbol keys as well as string keys which were previously returned by keyof. Playground link
If you want to stick to 2.8 for some reasons, you can use the workaround of handling arrays explicitly in DeepRequired using a conditional type.
export type DeepRequired<T> = {
[P in keyof T]-?: T[P] extends Array<infer U>?Array<DeepRequired<U>>: DeepRequired<T[P]>;
};

Typescript definition for union-type js

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

Categories