I'm trying to construct a map of types and callbacks so I started with an structure like this:
type Instantiable<T = unknown> = new (...args: any[]) => T;
type SingleParamFn<TInput = unknown, TOuput = unknown> = (arg: TInput) => TOuput;
type TypesRecord<T> = { type: Instantiable<T>, callback: SingleParamFn<T> };
type TypesMap = TypesRecord[]; // This is the structure
An example of this:
const myMap: TypesMap = [{ type: Date, callback: (value) => 'this is a Date' }];
This seems works as expected but I realised it is a bit inefficient as you need to loop the array to find the type you want.
I started to play with having the types as object keys and it seems to work at least in Google Chrome:
class MyFancyObject {}
class MyOtherObject {}
const map = {
[Date]: () => 'this is a date',
[MyFancyObject]: () => 'this is a fancy object',
[MyOtherObject]: () => 'this is another object'
};
console.log(map[Date]()); // 'this is a date'
console.log(map[MyFancyObject]()); // 'this is a fancy object'
console.log(map[MyOtherObject]()); // 'this is another object'
const obj = new MyFancyObject();
console.log(map[obj.constructor]()); // 'this is a fancy object'
console.log(map[(new Date()).constructor]()); // 'this is a date'
I've been checking some documentation and it seems like an object's key can only be an string or a Symbol so I checked what constitutes a Symbol and it seems to be a bit vage as it is defined as a built-in object whose constructor returns a symbol primitive that's guaranteed to be unique and an object's constructor is (to the best of my knowledge), unique.
Given all of this, is using the constructors as object keys a reliable approach? If so, how can I define the typescript type for such object? If it isn't, are there other options (like the Map
object)?
CodePudding user response:
Given all of this, is using the constructors as object keys a reliable approach?
No. As you said, an object's keys can only be strings or Symbols. A Symbol is a primitive you get from a property on Symbol
(for well-known, specification-defined Symbols) or by calling Symbol()
or Symbol.for("...")
. Constructor functions are not symbols, they're functions.
Your map
object's keys are strings. When you use a computed property name like [MyFancyObject]
, if the value you provide is not a string or a Symbol (in your case, it's a function), it's converted to string implicitly as though you did String(MyFancyObject)
. You can see that here:
class MyFancyObject {}
class MyOtherObject {}
const map = {
[Date]: () => 'this is a date',
[MyFancyObject]: () => 'this is a fancy object',
[MyOtherObject]: () => 'this is another object'
};
console.log(Object.keys(map));
console.log(map[MyFancyObject]()); // "this is a fancy object"
console.log(map["class MyFancyObject {}"]()); // also "this is a fancy object"
As you can see, the key you're thinking of as MyFancyObject
is actually the string "class MyFancyObject {}"
.
You can use constructor functions as keys in a Map
, though:
class MyFancyObject {}
class MyOtherObject {}
const map = new Map([
[Date, () => 'this is a date'],
[MyFancyObject, () => 'this is a fancy object'],
[MyOtherObject, () => 'this is another object'],
]);
console.log(map.get(MyFancyObject)());
In that, the keys really are the constructor functions, not strings derived from them.