I am trying to write a wrapper for all my third party API interfaces and SDKs that logs request in standardized, yet customizable way. The way I would like to do this is pass the third party API (usually instatiated with a new API() call) into a wrapper class (APIClient). This client receives an object with certain methods on the third party API mapped to logging functions (that way I can say I can specify when it needs to santize PII, for example). It iterates over this object and redefines on its own this the methods defined on the third party API, calling the logging function after it invokes the third party method. This way the API can have the same interface as the third party API, with the benefit of custom behavior.
I have been struggling for a long time with the typings on this, of course, and feel like I may be close to getting it to work, but I can't quite get it over the line. I was inspried by TS docs section on "Mixins" but I'm not sure that is the way to go.
Some really confusing errors I'm getting:
Type 'Function' provides no match for the signature '(...args: any): any'.
No index signature with a parameter of type 'string' was found on type 'ApiClient<T>'.
(The second one is less confusing, I know that Object.entries gives key values pairs as strings and values, but I am stuck on what else to do)
Does anybody see what might be going wrong here, and how I might fix it? Thank you.
type Constructor = new (...args: any[]) => {};
type Method<T, K extends keyof T> = T[K] extends Function ? T[K] : never;
class ApiClient<T extends Constructor> {
_api: T;
constructor(api: T, logConfig: Record<keyof T, () => void>) {
this._api = api;
for (const [method, fn] of Object.entries(logConfig)) {
this[method] = this.createWrappedMethod(method, fn)
}
}
createWrappedMethod<
N extends keyof InstanceType<T>,
M extends Method<InstanceType<T>, N>,
>(name: N, logFn: () => void) {
return async (...args: Parameters<M>) => {
try {
const res = await this._api[name](...args);
// do logging
} catch {
// handle error`
}
};
}
}
I'm not sure how to fix the typing issues with your current approach, however, it seems like you are doing something quite similar to what Proxy objects are designed for.
In short, Proxy objects let you redefine certain operations on objects like property accesses, which consequently allows you to wrap and overwrite functions to insert logging and sanitation.
For example, here is an object wrapped in a Proxy that simply prints the result of a method call to the console:
const api = new API();
const proxy = new Proxy(api, {
get(target, prop) {
if (typeof target[prop] !== "function") {
return target[prop];
}
return async (...args) => {
const res = await target[prop](...args);
console.log(res)
// do more stuff...
return res;
};
},
});
This also automatically works with TypeScript; the compiler will type and recognize Proxy objects as the type of the original object. In other words, in TypeScript, a Proxy(new API(), {...}) is still an API.
Related
I am attempting to author a class that will give me type safety for each key + value specified in an interface and passed as a generic.
I've managed to get everything working EXCEPT that I am unable to ensure that a key belongs to the interface. On other words: I can pass string values that ARE NOT found within the interface provided to a generic.
I just cannot figure out how to adjust my code so that I can guarantee the key exists in the interface.
An ultra-minimal version of the code:
type Listener<V = unknown[]> = (...values: V[]) => void;
type EventMap = Record<string, Listener>;
type Library<T> = Partial<Record<keyof T, Set<T[keyof T]>>>;
class EventBlaster<T extends EventMap> {
lib: Library<T> = {};
register<K extends keyof T>(eventName: K, listener: T[K]) {}
trigger<K extends keyof T>(eventName: K, ...values: Parameters<T[K]>) {}
}
///
/// Usage
type CustomMap = EventMap & {
change(value: string): void;
};
const customEvents = new EventBlaster<CustomMap>();
// Working register / trigger
customEvents.register('change', (value) => {});
customEvents.trigger('change', 'hello');
// TypeScript correctly throwing errors on wrong listener arguments
customEvents.trigger('change', '1st string', '2nd string');
// TypeScript failing to throw errors on undeclared event names
customEvents.register('nope', () => {});
customEvents.trigger('nope');
In that example, I've got all of the listener > argument values working as expected - which is great and I assumed that would be the hard part...
But unfortunately, you can see that I am able to call register / trigger with string values that are not keys on the CustomMap type.
Any help in resolving this would be greatly appreciated! It would also be great if I didn't have to extend EventMap every time I defined a interface to provide as the generic argument for EventBlaster... but my assumption is that this is a requirement to get the behaviour that I am after.
A slightly more robust example of the code can be found in this TypeScript Playground link.
I am migrating a Nodejs project from JavaScript to TypeScript and I keep getting this error from the code that worked earlier in JavaScript. I have some functions defined in a separated module, I want to access this function from a function in another module depending on the arguments passed to the function.
schemas/users.ts
export const signup = () => {}
export const signin = () => {}
schemas/index.ts
import * as UserSchemas from './users';
export { UserSchemas }
validate.ts
import * as schemas from './schemas';
const validate = (schemaCollection: string, schemaName: string) => {
const schemaFunc = schemas[schemaCollection][schemaName];
// Use the schemaFunc in the code following this line.
}
If the validate function is passed called like this: validate('UserSchemas', 'signin'), I want to get the signin function back. I get this error when I run the code:
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'typeof import("...schemas/index")'.
No index signature with a parameter of type 'string' was found on type 'typeof import("...schemas/index")'. ts(7053)
The error is basically saying that your typing of validate is too lenient, for example validate("Does", "not exist") is valid, but schemas[schemaCollection][schemaName] will throw an error/return undefined.
The correct way to circumvent this is either a union of literals (or enums), or generics:
const validate = <T extends keyof typeof schemas, K extends keyof typeof schemas[T]>(schemaCollection: T, schemaName: K) => {
const schemaFunc = schemas[schemaCollection][schemaName];
// Use the schemaFunc in the code following this line.
}
Playground
Although, if this function uses user supplied data in any way, hard code the possible paths or thoroughly validate the input before using this since this is only a compile-time measure and does not protect you from a user entering invalid information and crashing your application.
I am using React and every time something changes it renders the whole component. Although I have a smallest possible component, but it runs a heavy function. I don't want this function to run on every render if its parameters has not changed. Something like React.memo (basically don't re-render a component if its props have not changed). Is there an equivalent of React.memo for static JavaScript functions?
NOTE: I don't want to pull in a library such as reselect. There has to be a better way!
Edit: I feel like I was not very clear about what I was looking for. Let me ask this with an example,
https://codesandbox.io/s/react-typescript-2xe2m?expanddevtools=1&fontsize=14&hidenavigation=1&theme=dark
Every time I click on + or -, it runs the pleaseMemoizeThisFunction function, even though its parameter has not changed. How can I have this function only runs when any of its parameters change.
Use the useMemo hook around your functions and it will not run unless the params have changed.
https://reactjs.org/docs/hooks-reference.html#usememo
const computed = useMemo(() => calculateExpensive(param1, param2), [param1, param2]);
This is a simple memoization implementation that checks wether the function is called with the same arguments as the last time, and only recalculates the result if they differ.
Seing in your sandbox that you use Typescript, I've added the Types to this function.
function memo<A extends any[], R>(fn:(...args:A) => R) {
let value:R,
before: A = {length:NaN} as any;
const sameAsBefore = (v:A[number], i:number) => v === before[i];
function memoized (...args:A):R {
if (args.length !== before.length || !args.every(sameAsBefore)) {
before = args;
value = fn.apply(this, args);
}
return value;
}
}
usage:
const pleaseMemoizeThisFunction = memo((a: string, b: string): string => {
console.count("function run: ");
// it will do some heavy stuff here
return `I am done with ${a} and ${b}`;
});
or like this.someMethod = memo(this.someMethod);
I am working with NestJS and to make testing easier I am trying to create something like Moq in .NET which gives complete mock based on the class with all methods mocked etc.
To clarify: when I have ClassA with methods testA and testB, I want my function to return an object which has these methods mocked with jest.fn() without having to create these objects every time by the hand.
I can get it working with pure JS without any issues:
export function getMockedClass<T>(classDef: T): MyAutoMock<T> {
const methods = Object.getOwnPropertyNames(classDef);
const constructorIndex = methods.findIndex((m) => 'constructor');
if (constructorIndex >= 0) {
methods.slice(constructorIndex, 1);
}
const mock: any = {};
methods.forEach((methodName) => {
mock[methodName] = jest.fn();
});
return mock;
}
However I am struggling to create type for MyAutoMock to get it accepted by TS as proper instance of the given class. Replacing MyAutoMock<T> with just :T obviously does not work.
Anyone with an idea?
I saw this block of code in our code base and I have a bit of a problem to understand void = (page). According to https://stackoverflow.com/a/34274584/513413, the return type is coming after => which is void in my case. So what does = (page) => {} do?
What is its equivalent function if I don't write it with fat arrow function?
This is the code:
private navigateTo: (page: string) => void = (page) => {
// display page
}
You are looking at the code incorrectly. The general structure is
private Name: Type = Value
The type is (page: string) => void and the value is (page) => {}. The type means that navigateTo is a function that accepts a string as argument and returns nothing, which is what (page) => {} does.
In Typescript, typings are inserted inside the statements of the language, transforming them a bit.
The code you submitted should read as such:
private navigateTo: This part is straighforward. We create a private member called navigateTo inside the current class.
...: (page: string) => void: This is the type of the member. In this case, it represents a function taking in a string parameter and not returning anything (void). This part is purely Typescript.
... = (page) => { /* display page */ }: This is the actual function being assigned to the variable.
I recommend you read some of the Typescript Handbook. It has a lot of information about the syntax and the language.