Map default value - javascript
I'm looking for something like default value for Map.
m = new Map();
//m.setDefVal([]); -- how to write this line???
console.log(m[whatever]);
Now the result is Undefined but I want to get empty array [].
First of all to answer the question regarding the standard Map: Javascript Map as proposed in ECMAScript 2015 does not include a setter for default values. This, however, does not restrain you from implementing the function yourself.
If you just want to print a list, whenever m[whatever] is undefined, you can just:
console.log(m.get('whatever') || []);
as pointed out by Li357 in his comment.
If you want to reuse this functionality, you could also encapsulate this into a function like:
function getMapValue(map, key) {
return map.get(key) || [];
}
// And use it like:
const m = new Map();
console.log(getMapValue(m, 'whatever'));
If this, however, does not satisfy your needs and you really want a map that has a default value you can write your own Map class for it like:
class MapWithDefault extends Map {
get(key) {
if (!this.has(key)) {
this.set(key, this.default());
}
return super.get(key);
}
constructor(defaultFunction, entries) {
super(entries);
this.default = defaultFunction;
}
}
// And use it like:
const m = new MapWithDefault(() => []);
m.get('whatever').push('you');
m.get('whatever').push('want');
console.log(m.get('whatever')); // ['you', 'want']
As of 2022, Map.prototype.emplace has reached stage 2.
As it says on the proposal page, a polyfill is available in the core-js library.
For my purposes, I thought it would be more clear to have a DefaultMap class that extended a normal Map and added additional methods. This is really nice because it leads to more declarative code. Meaning that when you declare a new Map, not only do you declare the type of the keys and the type of the values, you also declare the default value.
As a quick example:
// Using a primitive as a default value
const myMap1 = new DefaultMap<string, number>(123);
const myMap1Value = myMap1.getAndSetDefault("some_key");
// Using a factory function to generate a default value
const myMap2 = new DefaultMap<string, number, [foo: Foo]>((_key, foo) => foo.bar);
const foo = new Foo();
const myMap2Value = myMap2.getAndSetDefault("some_key", foo);
The code is below:
type FactoryFunction<K, V, A extends unknown[]> = (k: K, ...extraArgs: A) => V;
type FirstArg<K, V, A extends unknown[]> =
| Iterable<[K, V]>
| V
| FactoryFunction<K, V, A>;
type SecondArg<K, V, A extends unknown[]> = V | FactoryFunction<K, V, A>;
interface ParsedArgs<K, V, A extends unknown[]> {
iterable: Iterable<[K, V]> | undefined;
defaultValue: V | undefined;
defaultValueFactory: FactoryFunction<K, V, A> | undefined;
}
/**
* An extended Map with some new methods:
*
* - `getAndSetDefault` - If the key exists, this will return the same thing as the `get` method.
* Otherwise, it will set a default value to the key, and then return the default value.
* - `getDefaultValue` - Returns the default value to be used for a new key. (If a factory function
* was provided during instantiation, this will execute the factory function.)
* - `getConstructorArg` - Helper method for cloning the map. Returns either the default value or
* the reference to the factory function.
*
* When instantiating a new DefaultMap, you must specify either a default value or a function that
* returns a default value.
*
* Example:
* ```ts
* // Initializes a new empty DefaultMap with a default value of "foo"
* const defaultMapWithPrimitive = new DefaultMap<string, string>("foo");
*
* // Initializes a new empty DefaultMap with a default value of a new Map
* const defaultMapWithFactory = new DefaultMap<string, Map<string, string>>(() => {
* return new Map();
* })
*
* // Initializes a DefaultMap with some initial values and a default value of "bar"
* const defaultMapWithInitialValues = new DefaultMap<string, string>([
* ["a1", "a2"],
* ["b1", "b2"],
* ], "bar");
* ```
*
* If specified, the first argument of a factory function must always be equal to the key:
*
* ```ts
* const defaultMapWithConditionalDefaultValue = new DefaultMap<number, number>((key: number) => {
* return isOdd(key) ? 0 : 1;
* });
* ```
*
* You can also specify a factory function that takes a generic amount of arguments beyond the
* first:
*
* ```ts
* const factoryFunction = (_key: string, arg2: boolean) => arg2 ? 0 : 1;
* const defaultMapWithExtraArgs = new DefaultMap<string, string, [arg2: boolean]>(factoryFunction);
* ```
*/
export class DefaultMap<K, V, A extends unknown[] = []> extends Map<K, V> {
private defaultValue: V | undefined;
private defaultValueFactory: FactoryFunction<K, V, A> | undefined;
/**
* See the DefaultMap documentation:
* [insert link here]
*/
constructor(
iterableOrDefaultValueOrDefaultValueFactory: FirstArg<K, V, A>,
defaultValueOrDefaultValueFactory?: SecondArg<K, V, A>,
) {
const { iterable, defaultValue, defaultValueFactory } = parseArguments(
iterableOrDefaultValueOrDefaultValueFactory,
defaultValueOrDefaultValueFactory,
);
if (defaultValue === undefined && defaultValueFactory === undefined) {
error(
"A DefaultMap must be instantiated with either a default value or a function that returns a default value.",
);
}
if (iterable === undefined) {
super();
} else {
super(iterable);
}
this.defaultValue = defaultValue;
this.defaultValueFactory = defaultValueFactory;
}
/**
* If the key exists, this will return the same thing as the `get` method. Otherwise, it will set
* a default value to the key, and then return the default value.
*/
getAndSetDefault(key: K, ...extraArgs: A): V {
const value = this.get(key);
if (value !== undefined) {
return value;
}
const defaultValue = this.getDefaultValue(key, ...extraArgs);
this.set(key, defaultValue);
return defaultValue;
}
/**
* Returns the default value to be used for a new key. (If a factory function was provided during
* instantiation, this will execute the factory function.)
*/
getDefaultValue(key: K, ...extraArgs: A): V {
if (this.defaultValue !== undefined) {
return this.defaultValue;
}
if (this.defaultValueFactory !== undefined) {
return this.defaultValueFactory(key, ...extraArgs);
}
return error("A DefaultMap was incorrectly instantiated.");
}
/**
* Helper method for cloning the map. Returns either the default value or a reference to the
* factory function.
*/
getConstructorArg(): V | FactoryFunction<K, V, A> {
if (this.defaultValue !== undefined) {
return this.defaultValue;
}
if (this.defaultValueFactory !== undefined) {
return this.defaultValueFactory;
}
return error("A DefaultMap was incorrectly instantiated.");
}
}
function parseArguments<K, V, A extends unknown[]>(
firstArg: FirstArg<K, V, A>,
secondArg?: SecondArg<K, V, A>,
): ParsedArgs<K, V, A> {
return secondArg === undefined
? parseArgumentsOne(firstArg)
: parseArgumentsTwo(firstArg, secondArg);
}
function parseArgumentsOne<K, V, A extends unknown[]>(
firstArg: FirstArg<K, V, A>,
): ParsedArgs<K, V, A> {
const arg = firstArg as SecondArg<K, V, A>;
const { defaultValue, defaultValueFactory } =
parseDefaultValueOrDefaultValueFactory(arg);
return {
iterable: undefined,
defaultValue,
defaultValueFactory,
};
}
function parseArgumentsTwo<K, V, A extends unknown[]>(
firstArg: FirstArg<K, V, A>,
secondArg: SecondArg<K, V, A>,
): ParsedArgs<K, V, A> {
const firstArgType = type(firstArg);
if (firstArgType !== "table") {
error(
"A DefaultMap constructor with two arguments must have the first argument be the initializer list.",
);
}
const { defaultValue, defaultValueFactory } =
parseDefaultValueOrDefaultValueFactory(secondArg);
return {
iterable: firstArg as Iterable<[K, V]>,
defaultValue,
defaultValueFactory,
};
}
function parseDefaultValueOrDefaultValueFactory<K, V, A extends unknown[]>(
arg: SecondArg<K, V, A>,
): {
defaultValue: V | undefined;
defaultValueFactory: FactoryFunction<K, V, A> | undefined;
} {
if (typeof arg === "function") {
return {
defaultValue: undefined,
defaultValueFactory: arg as FactoryFunction<K, V, A>,
};
}
if (
typeof arg === "boolean" ||
typeof arg === "number" ||
typeof arg === "string"
) {
return {
defaultValue: arg as V,
defaultValueFactory: undefined,
};
}
return error(
`A DefaultMap was instantiated with an unknown type of: ${typeof arg}`,
);
}
Most straightforward implementation:
Here's a simple MapWithDefault class that extends the built-in Map.
The clever trick is here is using symbols for specifying the default value, resulting in a nice and readable syntax:
(The snippets below use TypeScript to provide type-safety, but this is optional, you can remove the type annotations if you wish.)
mapWithDefault.ts:
export const DEFAULT = Symbol();
export class MapWithDefault<K, V> extends Map<K | typeof DEFAULT, V> {
get(key: K): V {
return super.has(key)
? super.get(key)
: super.get(DEFAULT);
}
}
Usage:
import { MapWithDefault, DEFAULT } from './mapWithDefault';
const map = new MapWithDefault<number, string>([
[404, 'Not found!'],
[403, 'Forbidden!'],
[DEFAULT, 'Unknown error'],
]);
console.log(map.get(404)); // "Not found!"
console.log(map.get(123)); // "Unknown error"
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 } }; }, });
Typescriping keyof object with 1 property issue
So I've created a merge function which takes in an object & a key of another object of objects. The function then returns the merged objects as below. const data = { end: 'World' } const working = { test: 'Worked' } const map = { data, working } export type Map = typeof map; export type Keys = keyof Map; function merge<A, B extends Keys, C extends Map[B] & A>(input: { obj: A; append?: B; }) { const append = input.append ? map[input.append] : {}; return { ...input.obj, ...append } as C } Now this works perfectly fine when the map object has > 1 property. aka const initial = { start: 'Hello' } const a = merge({ obj: initial, append: 'data' ); const b = merge({ obj: initial }); // Available as expected console.log(a.end); // Errors as expected console.log(b.end); However if I change the map object to only have 1 property like so const map = { data } Then this occurs const a = merge({ obj: initial, append: 'data' ); const b = merge({ obj: initial }); // Available as expected console.log(a.end); // Available but outputs undefined console.log(b.end); I kind of understand what is going on. When the map object only has 1 property, the generic is defaulting to that key so TypeScript believes the object is merged when it isn't. How do I avoid this?
export type Map = typeof map; export type Keys = keyof Map; function merge<A, B extends C extends keyof Map ? A & Map[C] : A, C extends undefined | keyof Map = undefined>(input: { obj: A; append?: C; }) { const append = input.append ? map[input.append as keyof Map] : {}; return { ...input.obj, ...append } as B } const data = { end: 'World' } const map = { data } // Testing const start = { hello: 'World' } const a = merge({ obj: start }); // Outputs as expected console.log(`${a.hello} World`); // Errors as expected console.log(`${a.hello} ${a.end}`); const b = merge({ obj: start, append: 'data' }) // Outputs as expected console.log(`${b.hello} ${b.end}`); So this is how I've gotten it working. Basically C extends undefined | keyof Map and has a default type of undefined so by default the returned type is A & undefined. When a value which must be keyof Map is passed as the append then C becomes that type and thus the returned type is Map[C] & A
Typesafe call some function from object of functions
I want to create an object of functions(all functions have 1 parameter). And make another function that can typesafe pass parameters from the outside call to one of the functions from an object. const double = (v: number) => v * v const concat = (v: string) => v + v const functions = { double, concat } const execute = <T extends keyof typeof functions> (key: T, param: Parameters<typeof functions[T]>[0]) => { functions[key](param) // here i can't match param type to function argument type, and getting an error } execute('double', 'str') // here everything is fine i get correct TypeError TS playground How to solve that?
We can assert that functions[key] takes argument of type param so that TS gets confirmed that function gets correct param type all the time at runtime. const double = (v: number) => v * v const concat = (v: string) => v + v const functions = { double, concat } const execute = <T extends keyof typeof functions> (key: T, param: Parameters<typeof functions[T]>[0]) => { (functions[key] as (v:typeof param)=>typeof param)(param) } execute('double', 5) execute('double', 'ram) // error execute('concat', 'ram') execute('concat', 5) // error
Typescript limitation workaround
I have a code, that is totaly OK from the human perspective. But it looks like typescript type system have hard time to understand it. Is there a smart way to hint compiler that everything is fine in that line? const isMustToRun: boolean = isFunc(condition) ? condition() : condition; code: export const noop = function() { }; export const isFunc = function(obj: any): boolean { return typeof obj === 'function'; }; /** * * #param func funtion to run * #param condition condition check * #param args */ export const runIf = function f(condition: (Function | boolean), func: Function, ...args: any[]) { return () => { const isMustToRun: boolean = isFunc(condition) ? condition() : condition; return isMustToRun ? func(...args) : noop(); }; }; If I wrote typeof condition === 'function' instead of "isFunc" call, then it works. But I don't want to repeat the code..
Change the return type of export const isFunc = function(obj: any): boolean { return typeof obj === 'function'; }; to be is Function export const isFunc = function(obj: any): obj is Function { return typeof obj === 'function'; }; More This is called a user defined type guard 🌹
You need to transform isFunc in a custom type guard and your code will work as expected.: export const isFunc = function(obj: any): obj is Function { return typeof obj === 'function'; }; I would recommend not using Function though as it is not really very type safe, you can tighten up the function type with a function signature, and use the Extract conditional type to preserve the actual type of the passed in function to the isFunc type-guard: export const isFunc = function<T>(obj: T): obj is Extract<T, Function> { return typeof obj === 'function'; }; export const runIf = function f(condition: ((()=> boolean) | boolean), func: Function, ...args: any[]) { return () => { const isMustToRun: boolean = isFunc(condition) ? condition() : condition; return isMustToRun ? func(...args) : noop(); }; }; Or a fully type safe version of runIf that checks the args agains the func parameters: export const runIf = function f<T extends (...a: any)=>any>(condition: ((()=> boolean) | boolean), func: T, ...args: Parameters<T>): (()=>ReturnType<T>) { return () => { const isMustToRun: boolean = isFunc(condition) ? condition() : condition; return isMustToRun ? func(...args as any[]) : noop(); }; }; function testFn(n: string) : number { return +n;} runIf(true, testFn, "0"); //ok runIf(true, testFn, 0); //err
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