Skip to content

Commit

Permalink
Fix mw.Map autocompletion
Browse files Browse the repository at this point in the history
when `V` may include unknown additional keys
We may want to use `Map<V> & Map<Record<string, unknown>>`, but this alternative does not allow mixed autocompletion & typeckecking when passing a key array to `get` or an object to `set`.
  • Loading branch information
Adrien LESÉNÉCHAL committed Feb 9, 2024
1 parent 278d64d commit e47e9ae
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 11 deletions.
79 changes: 70 additions & 9 deletions mw/Map.d.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,68 @@
type KeyOrArray<T> = keyof T | Array<keyof T>;
type GetOrDefault<V, K extends keyof V, T> = V extends Required<Pick<V, K>>
? V[K]
: Required<V>[K] | T;
type PickOrDefault<V, S extends keyof V | Array<keyof V>, T> = S extends Array<infer SS>
? { [K in SS & keyof V]-?: GetOrDefault<V, K, T> }
: GetOrDefault<V, S & keyof V, null>;
type TypeOrArray<T> = T | T[];

// Get/PickOrDefault<V, S, TD, TX> extracts values from V using key selection S
// - TD is the value type of missing properties
// - TX is the value type of unknown properties

type GetOrDefault<V, K extends PropertyKey, TD, TX = unknown> = K extends keyof V
? V extends Required<Pick<V, K>>
? V[K]
: Required<V>[K] | TD
: TX | TD;

type PickOrDefault<V, S extends TypeOrArray<PropertyKey>, TD, TX = unknown> = S extends Array<
infer K
>
? { [P in K & PropertyKey]-?: GetOrDefault<V, P, TD, TX> }
: GetOrDefault<V, S & PropertyKey, TD, TX>;

// `ExtensibleMap<V, TX>` is an alternative to `Map<V & { [k: string]: TX; }>`
// but unlike the latter, ExtensibleMap provides additional overloads to improve selection
// autocompletion and type checking.

export interface ExtensibleMap<V extends Record<string, any>, TX = unknown> extends mw.Map<V> {
/**
* Check if a given key exists in the map.
*
* @param selection Key to check
* @returns True if the key exists
* @see https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/mw.Map-method-exists
*/
exists<S extends keyof V>(selection: S): selection is S;
exists<S extends string>(selection: S): selection is S;

/**
* Get the value of one or more keys.
*
* If called with no arguments, all values are returned.
*
* @param selection Key or array of keys to retrieve values for.
* @param fallback Value for keys that don't exist.
* @returns If selection was a string, returns the value. If selection was an array, returns
* an object of key/values. If no selection is passed, a new object with all key/values is returned.
* @see https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/mw.Map-method-get
*/
get<S extends TypeOrArray<keyof V>, TD>(
selection: S,
fallback: TD
): PickOrDefault<V, S, TD, TX>;
get<S extends TypeOrArray<string>, TD>(selection: S, fallback: TD): PickOrDefault<V, S, TD, TX>;
get<S extends TypeOrArray<keyof V>>(selection: S): PickOrDefault<V, S, null, TX>;
get<S extends TypeOrArray<string>>(selection: S): PickOrDefault<V, S, null, TX>;
get<T extends Record<string, any> = V | Record<string, TX>>(): T;

/**
* Set the value of one or more keys.
*
* @param selection Key to set value for, or object mapping keys to values
* @param value Value to set (optional, only in use when key is a string)
* @returns True on success, false on failure
* @see https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/mw.Map-method-set
*/
set<S extends keyof V>(selection: S, value: V[S]): boolean;
set<S extends string>(selection: S, value: TX): boolean;
set<S extends Partial<V> & Record<string, TX>>(selection: S): boolean;
}

declare global {
namespace mw {
Expand Down Expand Up @@ -40,8 +98,11 @@ declare global {
* an object of key/values. If no selection is passed, a new object with all key/values is returned.
* @see https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/mw.Map-method-get
*/
get<S extends KeyOrArray<V>, T>(selection: S, fallback: T): PickOrDefault<V, S, T>;
get<S extends KeyOrArray<V>>(selection: S): PickOrDefault<V, S, null>;
get<S extends TypeOrArray<keyof V>, TD>(
selection: S,
fallback: TD
): PickOrDefault<V, S, TD>;
get<S extends TypeOrArray<keyof V>>(selection: S): PickOrDefault<V, S, null>;
get<T extends V = V>(): T;

/**
Expand Down
5 changes: 3 additions & 2 deletions mw/config.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ExtensibleMap } from "./Map";

declare global {
namespace mw {
/**
Expand All @@ -11,7 +13,7 @@ declare global {
*
* @see https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/mw-property-config
*/
const config: Map<{
const config: ExtensibleMap<{
/**
* Since MediaWiki 1.36+, 0 means debug mode is off, and a positive non-zero number means debug mode is on (e.g. 1 or 2).
*
Expand Down Expand Up @@ -358,7 +360,6 @@ declare global {
* @see https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#wgDiffNewId
*/
wgDiffNewId?: number;
[key: string]: unknown; // more config keys can be added by extensions
}>;
}
}
Expand Down

0 comments on commit e47e9ae

Please sign in to comment.