Typescript: Usage of Map<> with strictNullChecks - javascript

Given the following simple class:
class Observer {
private subscribers: Map<string, Array<((data: any) => void)>> = new Map();
public subscribe(event: string, callback: (data: any) => void) {
if (!this.subscribers.has(event)) {
this.subscribers.set(event, []);
}
this.subscribers.get(event).push(callback); //tsc says: Object is possibly 'undefined'
}
}
Furthermore, in tsconfig.json, the flags strictNullChecks and strict are enabled.
Although subscribers is checked for a key of the current event, the typescript compiler complains with the error message shown above (this.subscribers.get(event) is possibly undefined).
If I'm not completely wrong, this.subscribers.get(event) can never be undefined in this case.
How can I get rid of that message?

Typing of Map explicitly states that get can result in undefined:
interface Map<K, V> {
...
get(key: K): V | undefined;
...
}
That's why you're getting error with strictNullChecks enabled.
You can use non-null assertion operator to inform the compiler that you're sure that it actually has value:
this.subscribers.get(event)!.push(callback);
Another option (the better one in my opinion) is to refactor your code in following way:
public subscribe(event: string, callback: (data: any) => void) {
let callbacks = this.subscribers.get(event);
if (!callbacks) {
callbacks = []
this.subscribers.set(event, callbacks);
}
callbacks.push(callback);
}

Related

Wrapping class in Proxy object TS

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

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: annotate ES5 generic class

I have the following code, in a file named storage.ts. Its purpose is to serve as a storage device for multiple parts of my API.
interface ICache<T> {
[key: string]: T
}
declare class Cacher<T> {
constructor (initialCache: ICache<T>)
define (key: string, val: T): void
get (key: string): T
remove (key: string): void
clear (): void
load (cacheObj: ICache<T>): void
}
function Cacher<T> (this: any, initialCache: ICache<T>) {
var cache: ICache<T> = initialCache
this.define = function (key: string, val: T) {
cache[key] = val
}
this.get = function (key: string) {
if (cache[key]) {
return cache[key]
} else {
throw new Error ("Key '" + key + "' doesn't exist")
}
}
this.remove = function (key: string) {
delete cache[key]
}
this.clear = function () {
cache = {}
}
this.load = function (cacheObj: ICache<T>) {
cache = cacheObj
}
}
export default Cacher
Usage example (containers.ts):
import Cacher from './storage'
type TemplateFunction = (options: object, config: object) => string
var Templates = new Cacher<TemplateFunction>({})
Everything is all fine and great, VSCode and TSLint don't throw any errors, etc., until I try and build with TypeDoc. Then, containers.ts gives this error:
'new' expression, whose target lacks a construct signature, implicitly has an 'any' type.
and storage.ts gives this error twice, on line 6 and 15:
Duplicate identifier 'Cacher'
I might think this was just an issue with TypeDoc, but changing function Cacher<T> (this: any, initialCache: ICache<T>) { to export default function Cacher<T> (this: any, initialCache: ICache<T>) { and removing the last line gives this error:
Merged declaration 'Cacher' cannot include a default export declaration. Consider adding a separate 'export default Cacher' declaration instead.ts(2652)
I've spent hours trying to fix this. I've tried multiple different variations of this StackOverflow answer, but it doesn't allow for passing in a generic. Besides, I find let x = function() to be a lot less elegant than function x().
Random notes:
I replaced this: any with this: [an interface that represents this]. Same issue as before
For reasons I'd rather not go into, I do not want to convert it to a class
Possibly related to this error: https://github.com/microsoft/TypeScript/pull/3973
I'd love any help - sorry for the long post!

How to attach class method as jQuery EventHandler in TypeScript

I'm converting some JavaScript code to TypeScript and I can't figure out how the signature of an jQuery EventHandler should look like.
This is what I had in JavaScript but simplified to more generic terms (where I have some sort of pub-sub or observable pattern using custom events distributed via an element):
Observer.prototype._subscribe = function() {
this._row.bind('onItemChanged', this, this._onChangedHandler);
};
Observer.prototype._onChangedHandler= function(event, someString, someObject) {
var that = event.data;
if (someString === '42') {
that.coolMethod(someObject);
} else if (someString === '69') {
that.otherCoolMethod(someObject);
}
};
In another prototype I would call trigger to notify the observer with the event and at least 2 parameters of data (someString and someObject):
Subject.prototype.foo = function() {
// Trigger the event so Observer will be notified and pass in the string and the object (or any data whatsoever)
this._element.trigger("onItemChanged", ["42", this._data]);
};
Now I'm having a hard time to write this in TypeScript but this is what I thought it should lok like:
export class Observer {
private _subscribe (): void {
this._element.bind('onItemChanged', this, this._onChangedHandler);
}
private _onChangedHandler(event: JQueryEventObject, someString: string, someObject: FooBarClass) {
let that = event.data as Observer;
if (someString === '42') {
that.coolMethod(someObject);
} else if (someString === '69') {
that.otherCoolMethod(someObject);
}
}
}
This TypeScript doesn't compile but gives the following error:
Argument of type '(event: JQueryEventObject, someString: string, someObject: FooBarClass) => void' is not assignable to parameter of type 'EventHandler | EventHandlerBase>'.
Type '(event: JQueryEventObject, someString: string, someObject: FooBarClass) => void' is not assignable to type 'EventHandlerBase>'.
Types of parameters 'event' and 't' are incompatible.
Type 'Event' is not assignable to type 'JQueryEventObject'.
So how should the signature (parameter types) of the eventhandler look like?
P.S. If you like you can rewrite using jQuery::on() instead of jQuery::bind();
P.P.S. I'm not interested in the various ways how to get the proper 'this' unless I'm realy on the wrong path here.
Edit (after first replay):
As suggested I rewrote my TypeScript to make my problem more clear. I know of the fat arrow notation to get the right 'this' but like I previously said, I'm not interested in this. I want my method to be typesafe so, how should the signature of my handler method look like?
This indeed works:
private _subscribe (): void {
this._element.on('onItemChanged', () => this._onChangedHandler);
}
private _onChangedHandler(event: JQueryEventObject, someString: string, someObject: FooBarClass) {
}
But then I expected that this should work too:
private _subscribe(): void {
this._element.on('onItemChanged', (event: JQueryEventObject, someString: string, someObject: FooBarClass) => { this._onChangedHandler(event, someString, someObject); });
}
But I still can't get the correct Event type for the first parameter to get typesafety. With the 'any' type it does compile:
private _subscribe(): void {
this._element.on('onItemChanged', (event: any, someString: string, someObject: FooBarClass) => { this._onChangedHandler(event, someString, someObject); });
}
By putting the 'any' type and running in debugger I finally found out during runtime that the first parameter is of the 'JQuery.Event' type which also compiles correctly in TypeScript when using the type definitions of jQuery. Too bad tsc couldn't tell me this sooner at compile time (or I misinterpreted the error show in the question).
So this is what it should look like (regardless of using bind, fat arrow or your own way of preserving the right context/this):
export class Observer {
private _element: JQuery;
private _subscribe(): void {
this._element.on('onItemChanged', (event: JQuery.Event, someString: string, someObject: FooBarClass) => { this._onChangedHandler(event, someString, someObject); });
}
private _onChangedHandler(event: JQuery.Event, someString: string, someObject: FooBarClass): void {
}
}
Although this compiles and gives some sort of typesafety, I'm a bit dissapointed since I can attach handlers with the wrong signature and it still compiles. I guess that is just how attaching eventhandlers work. At least you can call your methods directly with typesafety and still use them as handlers.
So for others running into this issue the signature should look like this:
export class Observer {
private _element: JQuery;
private _subscribe(): void {
this._element.on('anyevent', () => this._yourOwnHandler);
}
private _yourOwnHandler(event: JQuery.Event, ...args: any[]): void {
}
}
Where the '...args' can be anything you like that will give you the number and types of parameters you want.
P.P.S. I'm not interested in the various ways how to get the proper
'this' unless I'm realy on the wrong path here.
I know you don't want to hear this, but when you are dealing with events, you really do need to take care of your scope.
this._element.bind('onItemChanged', this, () => {
this._onChangedHandler();
});
And I also recommend moving to on as bind is deprecated.
This feels like an odd answer as I think you suspected both of these already?

How to define type for a function callback (as any function type, not universal any) used in a method parameter

Currently I have type definition as:
interface Param {
title: string;
callback: any;
}
I need something like:
interface Param {
title: string;
callback: function;
}
but the 2nd one is not being accepted.
The global type Function serves this purpose.
Additionally, if you intend to invoke this callback with 0 arguments and will ignore its return value, the type () => void matches all functions taking no arguments.
Typescript from v1.4 has the type keyword which declares a type alias (analogous to a typedef in C/C++). You can declare your callback type thus:
type CallbackFunction = () => void;
which declares a function that takes no arguments and returns nothing. A function that takes zero or more arguments of any type and returns nothing would be:
type CallbackFunctionVariadic = (...args: any[]) => void;
Then you can say, for example,
let callback: CallbackFunctionVariadic = function(...args: any[]) {
// do some stuff
};
If you want a function that takes an arbitrary number of arguments and returns anything (including void):
type CallbackFunctionVariadicAnyReturn = (...args: any[]) => any;
You can specify some mandatory arguments and then a set of additional arguments (say a string, a number and then a set of extra args) thus:
type CallbackFunctionSomeVariadic =
(arg1: string, arg2: number, ...args: any[]) => void;
This can be useful for things like EventEmitter handlers.
Functions can be typed as strongly as you like in this fashion, although you can get carried away and run into combinatoric problems if you try to nail everything down with a type alias.
Following from Ryan's answer, I think that the interface you are looking for is defined as follows:
interface Param {
title: string;
callback: () => void;
}
You can define a function type in interface in various ways,
general way:
export interface IParam {
title: string;
callback(arg1: number, arg2: number): number;
}
If you would like to use property syntax then,
export interface IParam {
title: string;
callback: (arg1: number, arg2: number) => number;
}
If you declare the function type first then,
type MyFnType = (arg1: number, arg2: number) => number;
export interface IParam {
title: string;
callback: MyFnType;
}
Using is very straight forward,
function callingFn(paramInfo: IParam):number {
let needToCall = true;
let result = 0;
if(needToCall){
result = paramInfo.callback(1,2);
}
return result;
}
You can declare a function type literal also , which mean a function can accept another function as it's parameter. parameterize function can be called as callback also.
export interface IParam{
title: string;
callback(lateCallFn?:
(arg1:number,arg2:number)=>number):number;
}
Here's an example of a function that accepts a callback
const sqk = (x: number, callback: ((_: number) => number)): number => {
// callback will receive a number and expected to return a number
return callback (x * x);
}
// here our callback will receive a number
sqk(5, function(x) {
console.log(x); // 25
return x; // we must return a number here
});
If you don't care about the return values of callbacks (most people don't know how to utilize them in any effective way), you can use void
const sqk = (x: number, callback: ((_: number) => void)): void => {
// callback will receive a number, we don't care what it returns
callback (x * x);
}
// here our callback will receive a number
sqk(5, function(x) {
console.log(x); // 25
// void
});
Note, the signature I used for the callback parameter ...
const sqk = (x: number, callback: ((_: number) => number)): number
I would say this is a TypeScript deficiency because we are expected to provide a name for the callback parameters. In this case I used _ because it's not usable inside the sqk function.
However, if you do this
// danger!! don't do this
const sqk = (x: number, callback: ((number) => number)): number
It's valid TypeScript, but it will interpreted as ...
// watch out! typescript will think it means ...
const sqk = (x: number, callback: ((number: any) => number)): number
Ie, TypeScript will think the parameter name is number and the implied type is any. This is obviously not what we intended, but alas, that is how TypeScript works.
So don't forget to provide the parameter names when typing your function parameters... stupid as it might seem.
There are four abstract function types, you can use them separately when you know your function will take an argument(s) or not, will return a data or not.
export declare type fEmptyVoid = () => void;
export declare type fEmptyReturn = () => any;
export declare type fArgVoid = (...args: any[]) => void;
export declare type fArgReturn = (...args: any[]) => any;
like this:
public isValid: fEmptyReturn = (): boolean => true;
public setStatus: fArgVoid = (status: boolean): void => this.status = status;
For use only one type as any function type we can combine all abstract types together, like this:
export declare type fFunction = fEmptyVoid | fEmptyReturn | fArgVoid | fArgReturn;
then use it like:
public isValid: fFunction = (): boolean => true;
public setStatus: fFunction = (status: boolean): void => this.status = status;
In the example above everything is correct. But the usage example in bellow is not correct from the point of view of most code editors.
// you can call this function with any type of function as argument
public callArgument(callback: fFunction) {
// but you will get editor error if call callback argument like this
callback();
}
Correct call for editors is like this:
public callArgument(callback: fFunction) {
// pay attention in this part, for fix editor(s) error
(callback as fFunction)();
}
There are various ways to define function types; however, some are better than others.
Although it is possible to use Function, the JavaScript function object, don't do that.
Source: TypeScript ESLint plugin recommended rule ban-types
Avoid the Function type, as it provides little safety for the following reasons:
It provides no type safety when calling the value, which means it's easy to provide the wrong arguments.
It accepts class declarations, which will fail when called, as they are called without the new keyword.
TypeScript supports multiple other ways. Most commonly, function type expressions are used. This method highly resembles arrow functions.
If arguments and return values are known to be of a certain form, then they should be typed.
For example,
interface Param {
callback: (foo: string, bar: number) => void
}
Note that these types can be as complex as needed, for example using object types or types created from other types.
If the types are truly unknown then prefer unknown over any.
Source: TypeScript ESLint plugin recommended rule no-explicit-any
When any is used, all compiler type checks around that value are ignored.
From the TS docs,
unknown is the type-safe counterpart of any.
Thus, using spread syntax,
interface Params {
callback: (...args: unknown[]) => unknown
}
Hopefully, this will help...
interface Param {
title: string;
callback: (error: Error, data: string) => void;
}
Or in a Function
let myfunction = (title: string, callback: (error: Error, data: string) => void): string => {
callback(new Error(`Error Message Here.`), "This is callback data.");
return title;
}
Typescript: How to define type for a function callback used in a method parameter?
You can declare the callback as 1) function property or 2) method:
interface ParamFnProp {
callback: (a: Animal) => void; // function property
}
interface ParamMethod {
callback(a: Animal): void; // method
}
There is an important typing difference since TS 2.6:
You get stronger ("sound") types in --strict or --strictFunctionTypes mode, when a function property is declared. Let's take an example:
const animalCallback = (a: Animal): void => { } // Animal is the base type for Dog
const dogCallback = (d: Dog): void => { }
// function property variant
const param11: ParamFnProp = { callback: dogCallback } // error: not assignable
const param12: ParamFnProp = { callback: animalCallback } // works
// method variant
const param2: ParamMethod = { callback: dogCallback } // now it works again ...
Technically spoken, methods are bivariant and function properties contravariant in their arguments under strictFunctionTypes. Methods are still checked more permissively (even if not sound) to be a bit more practical in combination with built-in types like Array.
Summary
There is a type difference between function property and method declaration
Choose a function property for stronger types, if possible
Playground sample code
I've just started using Typescript and I've been trying to solve a similar problem like this; how to tell the Typescript that I'm passing a callback without an interface.
After browsing a few answers on Stack Overflow and GitHub issues, I finally found a solution that may help anyone with the same problem.
A function's type can be defined with (arg0: type0) => returnType and we can use this type definition in another function's parameter list.
function runCallback(callback: (sum: number) => void, a: number, b: number): void {
callback(a + b);
}
// Another way of writing the function would be:
// let logSum: (sum: number) => void = function(sum: number): void {
// console.log(sum);
// };
function logSum(sum: number): void {
console.log(`The sum is ${sum}.`);
}
runCallback(logSum, 2, 2);
In typescript 4.8, Function type gives error. Instead we can write the type explicitly as fn: () => void.
If you want to use args as well,
function debounce(fn: (...args: any[]) => void, ms = 300) {
If you are looking for input callback function like on change event then use the following
type Props = {
callBack: ChangeEventHandler<HTMLInputElement>;
}

Categories