在 TypeScript 中使用 Map,其值为泛型类型
我有以下代码:
class A<T> {
constructor(public value: T) {}
}
const map = new Map();
map.set('a', new A('a'));
map.set('b', new A(1));
const a = map.get('a');
const b = map.get('b');
目前,a 和 b 被推断为 any。如果我把它改为:
const map = new Map<string, A<????>>();
我需要传递 A 的泛型类型,但这样我就失去了 value 属性的类型。有办法实现吗?
const a = map.get('a') // should be inferred as A<string>
const b = map.get('b') // should be inferred as A<number>
答案#1
TypeScript 的类型系统主要在于限制值的类型在一段时间内不发生变化。因此,如果你写 let a: string="hey";,你就是在告诉编译器 a 是 并且将始终是 string 类型。你之后不能写 a=4;,否则会出现编译错误。如果你只是写 let b="hey"; 而没有注解类型,编译器会 推断 类型为 string,同样地,之后如果你写 b=4;,也会得到编译错误。如果你希望一个变量有时存储 string,有时存储 number,你应该在声明时自行注解为 string | number,例如 let c: string | number="hey";,然后 c=4; 就可以了。
因此默认情况下,如果你有一个对象,你需要在声明时提前告诉编译器属性的类型。 所以下列代码会给你一个错误:
let o = {}; // type {}
o.a = new A('a'); // error, {} has no "a" property
o.b = new A(1); // error, {} has no "b" property
而以下将有效:
let o2 = { a: new A('a'), b: new A(1) }; // okay
// let o2: { a: A<string>; b: A<number>; }
TypeScript 的一个特性使这种限制更容易处理,那就是控制流类型分析,编译器通过将一个值限制到子类型,从而暂时缩小其类型。如果我写 let c: string | number="hey",那么我之后可以写 c.toUpperCase() 而不会出错,因为编译器看到该赋值操作暂时将 c 从 string | number 缩小为 string。如果没有这种分析,你就需要使用类型断言,例如 (c as string)。 toUpperCase()。但即使是控制流类型分析也无法任意改变一个值的类型;它只能缩小类型(或者在重新赋值时可能重新扩展回注解/推断的类型)。因此,你可以用它向对象添加属性,但不能改变属性的类型,甚至不能删除属性。如果你想能够删除一个属性,那么你在添加该属性时需要将其设置为可选。
TypeScript 3.7 引入了断言函数,这些函数允许用户定义的函数根据其参数自定义控制流收窄。你可以使用它来编写一个自定义的 setProp 函数,该函数向现有对象添加属性。由于我知道你想删除这些属性,我将使它们作为可选属性添加:
function setProp<O extends object, K extends PropertyKey, V>(
obj: Partial<O>,
key: Exclude<K, keyof O>, value: V
): asserts obj is Partial<O & Record<K, V>> {
(obj as any)[key] = value as any;
}
它是这样工作的:
const map = {}
map.a = new A('a'); // error, can't do this
// but can do this:
setProp(map, 'a', new A('a'));
// now that it's been set you can re-set it to the same type
map.a = new A('b');
setProp(map, 'b', new A(1));
setProp(map, 'c', "a string");
你可以通过属性访问来获取和删除属性,请记住,由于它们是可删除的,在访问它们之前需要检查 undefined:
map.a && console.log(map.a.value); // b
map.b && console.log(map.b.value); // 1
delete map.b; // okay
map.c && console.log(map.c.toUpperCase()); // A STRING
所以这就是我可能会处理它的方式。 但是,最好还是在声明 map 时就提前设置类型。基于控制流的类型缩小不如一开始就注解类型那样稳健。
最后,如果你真的想使用 Map 而不是普通对象,你可以为 Map 编写自己的自定义类型定义,因为内置的 Map 类型定义假设所有值将是相同类型的。但这已经足够长了。在下面的 Playground 链接中,我提供了一个可能的实现以及上面代码的其余部分。
好的,希望这能给你一些灵感。祝你好运!
游乐场链接
