网站搜索

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>

操场