So another 🤯 about TypeScript. We use plain objects as arbitrary maps all the time, but TypeScript deems that usage (correctly) too flexible.
const map = {};
map.foo = 'foo'; // Error: Property 'foo' does not exist on type '{}'.Okay, then we need to type our object as a map:
const map: object = {};
map.foo = 'foo'; // Error: Property 'foo' does not exist on type '{}'.So, the object type apparently isn't what we think it is. We can be more explicit:
const map: { [key: string]: any } = {};
map.foo = 'foo'; // No problem!For convenience, we can define a Dictionary type, including with a generic type parameter
whose value is any by default.
// Place in app-global type definition file, so only need to define this once per application.
interface Dictionary<T = any> {
[key: string]: T;
}
const map: Dictionary = {};
map.foo = 'foo' // No problem!But is it safe?
const map: Dictionary = {};
map.bar.undefinedError // Unfortunately, no problem despite this being JavaScript error :(So, we need to enhance our map type with the possibility of undefined. In order to guarantee
this, we must ensure that undefined can't be set as a value, including as the generic value parameter.
// Helper union type of all but undefined
// https://github.com/microsoft/TypeScript/issues/7648#issuecomment-202637151
type notUndefined = string | number | boolean | symbol | object;
interface Dictionary<T extends notUndefined = notUndefined> {
[key: string]: T | undefined;
}
const map: Dictionary = {};
map.bar.undefinedError // Error: `bar` is possibly undefined!Now what about Object.entries() and Object.values()? Since we know a Dictionary can't have
a key that corresponds to an undefined value, these methods should be undefined-safe.
interface User {
username: string
}
const map: Dictionary<User> = {};
Object.values(map).forEach((user) => console.log(user.username)); // Error: Object is possibly undefinedWhat if Object.entries() and Object.values(), built in methods, could be aware of the
Dictionary interface and take appropriate action? Well, We can safely augment the built in types
in an entirely backwards-compatible manner 😵
// Place in app-global type definition file, so only need to define this once per application.
interface ObjectConstructor {
values<TDictionary>(
o: TDictionary extends Dictionary ? TDictionary : never
): TDictionary extends Dictionary<infer TElement> ? TElement[] : never;
entries<TDictionary>(
o: TDictionary
): [string, GetDictionaryValue<TDictionary>][];
}
// Later, in app code...
const map: Dictionary<User> = {};
Object.values(map).forEach((user) => console.log(user.username)); // No issues
map.ian.username // We still correctly have an error here: object is possibly undefinedThe long story short, this file global.d.ts installed at the app root enables an app-global
Dictionary type that allows using plain JavaScript objects as undefined-safe arbitrary maps:
type notUndefined = string | number | boolean | symbol | object;
interface Dictionary<T extends notUndefined = notUndefined> {
[key: string]: T | undefined;
}
interface ObjectConstructor {
values<TDictionary>(
o: TDictionary extends Dictionary ? TDictionary : never
): TDictionary extends Dictionary<infer TElement> ? TElement[] : never;
entries<TDictionary>(
o: TDictionary
): [string, GetDictionaryValue<TDictionary>][];
}
Could you have possibly skipped over defining
GetDictionaryValue?