typescript - 从集合中获取具有正确类型的值的 Getter 函数
我在 Typescript 中遇到了一个看似难以解决的问题。如果我构建一个其结构在类外定义的泛型集合,Typescript 在直接引用其值时能正确理解类型,但在使用 getter 函数时则不能。
在这个例子中,我想要一个集合,其中开发者必须定义里面的键,但几乎可以赋予它任何想要的形状。
首先,我已经为集合中的条目定义了一个通用类:
class Entry<T extends string | number> {
value?: T;
constructor(value: T) {
this.value = value;
}
}
接下来,我为集合本身定义了一个类:
export type CollectionState<
Keys extends { [key: string]: string },
> = {
[key in Keys[keyof Keys]]: Entry<string> | Entry<number>;
};
class Collection<
Keys extends { [key: string]: string },
State extends CollectionState<Keys>,
> {
state: State;
constructor(initialState: State) {
this.state = initialState;
}
}
在这里,我创建了一个集合来测试它:
enum MyKeys {
KEY1 = 'key1',
KEY2 = 'key2',
}
type MyKeysType = typeof MyKeys;
// Define a shape for the collection
interface StateDefinition {
[MyKeys.KEY1]: Entry<string>,
[MyKeys.KEY2]: Entry<number>,
}
// Define an initial state for the collection
const initialState: StateDefinition = {
[MyKeys.KEY1]: new Entry<string>('key1'),
[MyKeys.KEY2]: new Entry<number>(2),
};
// Create a new instance of the collection
const collection = new Collection<
MyKeysType,
StateDefinition
>(initialState);
到目前为止,一切都符合。当我在集合中引用变量并检查它们时,我看到 key1 是 Entry<string> 类型,而 key2 是 Entry<number> 类型。
// All checks out
const test1 = collection.state[MyKeys.KEY1]; // type is Entry<string>
const test2 = collection.state.key1; // type is Entry<string>
const test3 = collection.state[MyKeys.KEY2]; // type is Entry<number>
const test4 = collection.state.key2; // type is Entry<number>
当我尝试加入一个获取数据的函数时,会出现问题。在这个例子中我扩展了一个新类,但无论是否扩展,问题都会发生。
class ExtendedCollection<
Keys extends { [key: string]: string },
State extends CollectionState<Keys>,
> extends Collection<Keys, State> {
getEntry(key: Keys[keyof Keys]) {
return this.state[key];
}
}
我原本以为在调用这个函数时 TypeScript 会正确推断类型,但事实并非如此。
const collection2 = new ExtendedCollection<
MyKeysType,
StateDefinition
>(initialState);
// All good
const test5 = collection2.state[MyKeys.KEY1]; // type is Entry<string>
const test6 = collection2.state.key1; // type is Entry<string>
const test7 = collection2.state[MyKeys.KEY2]; // type is Entry<number>
const test8 = collection2.state.key2; // type is Entry<number>
// FAIL!!!
const test9 = collection2.getEntry(MyKeys.KEY1); // type is Entry<number> | Entry<string>
const test10 = collection2.getEntry(MyKeys.KEY2); // type is Entry<number> | Entry<string>
我甚至无法具体说明它应该是哪种类型:
/*
TS2322: Type 'Entry<string> | Entry<number>' is not assignable to type 'Entry<string>'.
Type 'Entry<number>' is not assignable to type 'Entry<string>'.
Type 'number' is not assignable to type 'string'.
*/
const test11: Entry<string> = collection2.getEntry(MyKeys.KEY1);
/*
TS2322: Type 'Entry<string> | Entry<number>' is not assignable to type 'Entry<number>'.
Type 'Entry<string>' is not assignable to type 'Entry<number>'.
Type 'string' is not assignable to type 'number'.
*/
const test12: Entry<number> = collection2.getEntry(MyKeys.KEY2);
我最后尝试的是创建一些 getEntry 函数的重载。
class ExtendedCollection2<
Keys extends { [key: string]: string },
State extends CollectionState<Keys>,
> extends Collection<Keys, State> {
getEntry(key: Keys[keyof Keys]): Entry<number>
getEntry(key: Keys[keyof Keys]): Entry<string>
getEntry(key: Keys[keyof Keys]): any {
return this.state[key];
}
}
它能编译,但我仍然没有得到预期的效果。现在我看到两个值都返回为 Entry<number>,即使其中一个本应是 Entry<string>。
const collection3 = new ExtendedCollection2<
MyKeysType,
StateDefinition
>(initialState);
// All good
const test13 = collection3.state[MyKeys.KEY1]; // type is Entry<string>
const test14 = collection3.state.key1; // type is Entry<string>
const test15 = collection3.state[MyKeys.KEY2]; // type is Entry<number>
const test16 = collection3.state.key2; // type is Entry<number>
// Does not infer the correct type based on the key provided
const test17 = collection3.getEntry(MyKeys.KEY1); // type is Entry<number>
const test18 = collection3.getEntry(MyKeys.KEY2); // type is Entry<number>
/*
TS2322: Type 'Entry<string> | Entry<number>' is not assignable to type 'Entry<string>'.
Type 'Entry<number>' is not assignable to type 'Entry<string>'.
Type 'number' is not assignable to type 'string'.
*/
const test19: Entry<string> = collection3.getEntry(MyKeys.KEY1);
// Okay
const test20: Entry<number> = collection3.getEntry(MyKeys.KEY2);
我如何创建一个函数,它可以获取一个条目,但不会将其作为集合中所有潜在类型的联合返回?
答案#1
除非函数是泛型的,否则 TypeScript 不会推断任何类型。你问题中的重载实际上没有起到作用,因为两个重载的参数类型相同。TypeScript 会选择第一个符合参数的重载,这导致返回类型总是Entry<number>。
为了获得所需的行为,我们需要将 key 的类型设为泛型。
class ExtendedCollection<
Keys extends { [key: string]: string },
State extends CollectionState<Keys>,
> extends Collection<Keys, State> {
getEntry<K extends Keys[keyof Keys]>(key: K) {
return this.state[key];
}
}
传递给 getEntry 的参数类型将用于 推断 K 的类型。我们不需要注释返回类型,因为 TypeScript 会友好地为我们推断出类型 State[K]。
const collection = new ExtendedCollection<MyKeysType, StateDefinition>(
initialState
);
const test1 = collection.getEntry(MyKeys.KEY1);
// ^? const test1: Entry<string>
const test2 = collection.getEntry(MyKeys.KEY2);
// ^? const test2: Entry<number>
操场
