Trying use Map for implementing dictionary
however despite typing all posible keys and using ReadonlyMap Map.get(key) returns valueType | underfined
type dictKeys = 'key1' | 'key2' | 'key3'
const dictObj:ReadonlyMap<DictKeys, string> = new Map<DictKeys, string>{
key1: 'value1',
key2: 'value2',
key3: 'value3',
} satisfies Record<dictKeys, string>
function testDictObj(key: dictKeys) {
const res = dictObj[key]
return res
}
const dictMap:ReadonlyMap<DictKeys, string> = new Map([
['key1', 'value1'],
['key2', 'value2'],
['key3', 'value3'],
])
function testDictMap(key: dictKeys) {
const res = dictMap.get(key)
return res
}
IMHO TS should know all needed tyes for narrowing and if there invalid key's type or there is missing prop key definition in Map - it gives a warning so const res = dictMap.get(key)
should give a 'string' type only
If using Object - all is fine but for dict it's more semantic to use Maps Yes I can use assertion "!" cos I am shure for result but it seems less neat (
Maybe there is already walkaroung or custom utility type fo it?
Edit to add The final solution to problem (to get Dictionary as ReadonlyMap instead as object and have strictly typed get() method which given proper key type return values withpout underfine) found as:
function getEntries<
T extends Record<PropertyKey, unknown>,
K extends keyof T,
V extends T[K]
>(o: T) { return Object.entries(o) as [K, V][] }
interface T_Dict<K extends PropertyKey, V> extends ReadonlyMap<K, V> {get(key: K): V}
export class DictObject<K extends PropertyKey, V> extends Map<K, V> implements T_Dict<K, V>{
private constructor(obj: Record<K, V>) {
super(getEntries(obj))
}
public static create<K extends PropertyKey, V>(obj: Record<K, V>) {
return new DictObject(obj) as T_Dict<K, V>
}
public override get(key: K): V {
if (!this.has(key)) throw new Error('Wrong key!')
return super.get(key) as V
}
}
Trying use Map for implementing dictionary
however despite typing all posible keys and using ReadonlyMap Map.get(key) returns valueType | underfined
type dictKeys = 'key1' | 'key2' | 'key3'
const dictObj:ReadonlyMap<DictKeys, string> = new Map<DictKeys, string>{
key1: 'value1',
key2: 'value2',
key3: 'value3',
} satisfies Record<dictKeys, string>
function testDictObj(key: dictKeys) {
const res = dictObj[key]
return res
}
const dictMap:ReadonlyMap<DictKeys, string> = new Map([
['key1', 'value1'],
['key2', 'value2'],
['key3', 'value3'],
])
function testDictMap(key: dictKeys) {
const res = dictMap.get(key)
return res
}
IMHO TS should know all needed tyes for narrowing and if there invalid key's type or there is missing prop key definition in Map - it gives a warning so const res = dictMap.get(key)
should give a 'string' type only
If using Object - all is fine but for dict it's more semantic to use Maps Yes I can use assertion "!" cos I am shure for result but it seems less neat (
Maybe there is already walkaroung or custom utility type fo it?
Edit to add The final solution to problem (to get Dictionary as ReadonlyMap instead as object and have strictly typed get() method which given proper key type return values withpout underfine) found as:
function getEntries<
T extends Record<PropertyKey, unknown>,
K extends keyof T,
V extends T[K]
>(o: T) { return Object.entries(o) as [K, V][] }
interface T_Dict<K extends PropertyKey, V> extends ReadonlyMap<K, V> {get(key: K): V}
export class DictObject<K extends PropertyKey, V> extends Map<K, V> implements T_Dict<K, V>{
private constructor(obj: Record<K, V>) {
super(getEntries(obj))
}
public static create<K extends PropertyKey, V>(obj: Record<K, V>) {
return new DictObject(obj) as T_Dict<K, V>
}
public override get(key: K): V {
if (!this.has(key)) throw new Error('Wrong key!')
return super.get(key) as V
}
}
Share Improve this question edited Jan 20 at 14:07 Poul asked Jan 18 at 16:45 PoulPoul 3693 silver badges13 bronze badges 9 | Show 4 more comments1 Answer
Reset to default 2While TypeScript can do magic stuff with type inference on the object
type, it has to follow the defined type definitions of the Map class. And both Map
and ReadonlyMap
define the get
method like this:
get(key: K): V | undefined;
So no way that TypeScript can know automatically that the value can't be undefined
.
You could extend the Map
class like this:
class StrictMap<K, V> extends Map<K, V> {
public override get(key: K): V {
const result = super.get(key);
if (result === undefined) {
throw Error("Key not found");
}
return result;
}
}
By using this implementation the get method never returns undefined
, it throws an exception instead.
And if you need to cast it to Readonly you can extend this interface as well:
interface ReadonlyStrictMap<K, V> extends ReadonlyMap<K, V> {
get(key: K): V;
}
Now your example works and get
returns string
:
const dictMap = new StrictMap([
['key1', 'value1'],
['key2', 'value2'],
['key3', 'value3'],
]) satisfies ReadonlyStrictMap<dictKeys, string>;
function testDictMap(key: dictKeys) {
const res = dictMap.get(key)
return res // <-- Type: string
}
(EDIT: The satisfies
in the example is not really needed, just kept it to be close to your own example.)
dictMap
is just of typeMap<DictKeys, string>
since you usedsatisfies
. Anyone can calldelete()
on it. If you annotatedictMap
asReadonlyMap<DictKeys, string>
then TS doesn't know the keys are all present. Both problems as shown in this playground link. Please edit to show a valid use case, or this question risks being closed as needing details or clarity. – jcalz Commented Jan 18 at 20:07