Reputation: 56
I am creating a proxy-based system to intercept all reads/writes to a certain model. I am having problems, however, with creating the inerface types the system represents.
The system will, on a read or write to the model, intercept the read/write with a proxy and create new proxies accordingly to be able to intercept all future read/write operations. The root (let's call it root
) proxy is cast to a type the proxy represents. This may be, for example:
interface Root {
foo: string;
bar: Map<OtherType>;
}
where
interface Map<T> {
[key: string]: T | T[] | (obj: T) => string;
insert: (obj: T) => string;
asArray: T[];
}
The problem lies in the interface Map<T>
. I would like the type of the actual map to be just T
. I think the problem is that when root.bar[x]
is requested where x = insert
, the type of the returned object is (obj: T) => string
. However, all keys in the map will be a string of length 8. What i'd actually like to do is something like this:
type ID = string[8]
interface Map<T> {
[key: ID]: T
insert: (obj: T) => string;
asArray: T[];
}
I cannot figure out how to:
I already looked in the typescript declarations file, where the Object type is defined. In this definition they seem to omit the map completely. Is this something that's implemented in the compiler/linter itself?
Does anyone know a fix or workaround to achieve something like this? I think it's pretty ugly to have to cast to a type T every time I do a map lookup.
Upvotes: 1
Views: 7905
Reputation: 328362
First, your use of the word Map
is problematic since, as @TJCrowder mentions, that term means something else. I will call your type MyMap
in what follows.
TypeScript doesn't (currently as of v2.5) recognize any subtype of string
corresponding to 8-character strings. You can do this:
type Id = string & { length: 8 };
which defines a type Id
as a particular type of string
, but TypeScript is not clever enough to understand when a string
is an Id
(so it will never automatically narrow a string
type to Id
), and it will not let you use Id
as the index type of an object. If you want to make a string
into an Id
you will have to do it with a type assertion:
function toId(str: string): Id {
if (str.length !== 8)
throw new Error("an Id must have exactly 8 characters");
return str as Id;
}
This might not be worth it to you.
Since TypeScript will not recognize the Id
as an index type, there's no way to type MyMap<T>
the way you want it: the index is a string
, and so are "insert"
and "asArray"
. The most TypeScript-friendly workaround (which plays nicely with the TypeScript type system and lets you reap its benefits) is to move the Id
-indexed properties to its own object. For example:
interface TSFriendlyMyMap<T> {
props: { [key: string]: T } // can't do key: Id
insert: (obj: T) => Id;
asArray: T[];
}
In this case, you would use props
to hold the properties. Using the map would be a little different:
declare const reMap: TSFriendlyMyMap<RegExp>;
const id: Id = reMap.insert(/abc/);
const arr: RegExp[] = reMap.asArray;
const regExp: RegExp = reMap.props[id] // instead of reMap[id]
but doesn't seem worse to me.
Another thing you could do is use function overloads to make MyMap
a callable function instead of an indexable object:
type CallableMyMap<T> = {
(k: 'insert'): (obj: T) => Id
(k: 'asArray'): T[];
(k: Id): T
}
This behaves more closely to what you wanted, in that TypeScript knows that an Id
yields a T
, while "insert"
and "asArray"
don't:
declare const reMap: CallableMyMap<RegExp>;
const id: Id = reMap('insert')(/abc/);
const arr: RegExp[] = reMap('asArray');
const regExp: RegExp = reMap(id);
But you're using function calls, which is kind of weird.
I recommend TSFriendlyMyMap
over CallableMyMap
, and it's a tossup whether Id
is preferable to just being careful with string
. But it's up to you. Anyway, hope that helps; good luck!
Upvotes: 2