Reputation: 1983
I am using the Map dictionary in TypeScript and I want to have the get and has properties to work case insensitive. How can I make this work ?
let envVariables = new Map<string, string>();
envVariables.set('OS', 'Windows_NT');
envVariables.set('USERNAME', 'SYSTEM');
if (this.envVariables.has('UserName')) {
// this should work with case insensitive search
}
In C# the Dictionary constructor just needs the StringComparer.OrdinalIgnoreCase and then the dictionary will be case insensitive.
Upvotes: 11
Views: 10547
Reputation: 71
This is my variant of a "true" Case Insensitive Map created using TypeScript: it contains a private map that stores original keys.
export class CaseInsensitiveMap<TKey, TVal> extends Map<TKey, TVal> {
private keysMap = new Map<TKey, TKey>();
constructor(iterable?: Iterable<[TKey, TVal]>){
super();
if (iterable) {
for (const [key, value] of iterable) {
this.set(key, value);
}
}
}
set(key: TKey, value: TVal): this {
const keyLowerCase = typeof key === 'string'
? key.toLowerCase() as any as TKey
: key;
this.keysMap.set(keyLowerCase, key);
return super.set(keyLowerCase, value);
}
get(key: TKey): TVal | undefined {
return typeof key === 'string'
? super.get(key.toLowerCase() as any as TKey)
: super.get(key);
}
has(key: TKey): boolean {
return typeof key === 'string'
? super.has(key.toLowerCase() as any as TKey)
: super.has(key);
}
delete(key: TKey): boolean {
const keyLowerCase = typeof key === 'string'
? key.toLowerCase() as any as TKey
: key;
this.keysMap.delete(keyLowerCase);
return super.delete(keyLowerCase);
}
clear(): void {
this.keysMap.clear();
super.clear();
}
keys(): IterableIterator<TKey> {
return this.keysMap.values();
}
*entries(): IterableIterator<[TKey, TVal]> {
const keys = this.keysMap.values();
const values = super.values();
for (let i = 0; i < super.size; i++) {
yield [keys.next().value, values.next().value];
}
}
forEach(callbackfn: (value: TVal, key: TKey, map: Map<TKey, TVal>) => void): void {
const keys = this.keysMap.values();
const values = super.values();
for (let i = 0; i < super.size; i++) {
callbackfn(values.next().value, keys.next().value, this);
}
}
[Symbol.iterator](): IterableIterator<[TKey, TVal]> {
return this.entries();
}
}
Upvotes: 7
Reputation: 222319
Map
doesn't support this behaviour. It can be extended in order for keys to be stored and looked up in case-insensitive fashion. Since Map
internally uses set
on construction, the constructor doesn't need to be augmented.
It's straightforward with TypeScript es6
or higher target because ES6 built-in classes support extends
:
class CaseInsensitiveMap<T, U> extends Map<T, U> {
set(key: T, value: U): this {
if (typeof key === 'string') {
key = key.toLowerCase() as any as T;
}
return super.set(key, value);
}
get(key: T): U | undefined {
if (typeof key === 'string') {
key = key.toLowerCase() as any as T;
}
return super.get(key);
}
has(key: T): boolean {
if (typeof key === 'string') {
key = key.toLowerCase() as any as T;
}
return super.has(key);
}
}
Native classes should be treated in a special way when being extended in TypeScript with es5
target:
interface CaseInsensitiveMap<T, U> extends Map<T, U> {}
class CaseInsensitiveMap<T, U> {
constructor(entries?: Array<[T, U]> | Iterable<[T, U]>) {
return Reflect.construct(Map, arguments, CaseInsensitiveMap);
}
set (key: T, value: U): this {
if (typeof key === 'string') {
key = key.toLowerCase() as any as T;
}
return Map.prototype.set.call(this, key, value) as this;
}
get (key: T): U | undefined {
if (typeof key === 'string') {
key = key.toLowerCase() as any as T;
}
return Map.prototype.get.call(this, key) as U;
}
has(key: T): boolean {
if (typeof key === 'string') {
key = key.toLowerCase() as any as T;
}
return Map.prototype.has.call(this, key) as boolean;
}
}
Upvotes: 19