0

我有一个保存项目列表的类,但不是将它们保存在一个平面数组中,而是将它们保存在一个对象映射中,其中每个属性代表一组项目。就像我们有一个汽车清单,我们会按制造商对它们进行分组。

// object map
interface IObjectMap<TValue> {
    [key: string]: TValue;
}

// object map; same as interface but build using keys union type
type ItemMap<TMapKeys extends keyof IObjectMap<unknown>, TValue> = Record<TMapKeys, TValue[]>;

// just a function taking one parameter and returning a result
type GetterFunc<TInput, TResult> = (item: TInput) => TResult;

// object map where the value type of properties are getter functions returning a string
type GetterMap<TMapKeys extends keyof IObjectMap<unknown>, TInput> = Record<TMapKeys, GetterFunc<TInput, string>>;

class GroupedItems<
    TItem, // item type
    TGroupKeys extends keyof IObjectMap<unknown> // object map keys
> {
    public groups: ItemMap<TGroupKeys, TItem> = {} as ItemMap<TGroupKeys, TItem>;
    public countryGetters: GetterMap<TGroupKeys, TItem> = {} as GetterMap<TGroupKeys, TItem>;

    public addItems(items: TItem[], getGroupKey: GetterFunc<TItem, TGroupKeys>): void {
        this.items.concat(items);
        this.items
            .forEach(item => {
                let name = getGroupKey(item);
                if (this.groups[name] === undefined) {
                    // create placeholder for items
                    this.groups[name] = [];
                }
                // Put the item in the group
                this.groups[name].push(item);
            });
    }

    public assignGetters(getters: GetterMap<TGroupKeys, TItem>) {
        this.countryGetters= getters;
    }
}

该类有两个泛型类型参数:

  • 我们将分组的项目类型(即Car
  • 组键(即'renault' | 'peugeot' | ...

然后类成员都被定义为对象映射:

  • groups将项目保留在组数组中
  • countryGetters也是一个与项目组具有相同属性的对象映射,但它们定义了一个函数,该函数返回传入的汽车的制造商国家

使用示例

上面的代码似乎没有错误,但随着它的使用,类型似乎没有得到应有的解析。当我尝试使用未在映射或联合类型的组键中定义的项目组名称时,我希望编译器/linter 抱怨...

interface Car {
    model: string;
    year: number;
}

interface CarMakers<TValue> extends IObjectMap<TValue> {
    renault: TValue;
    peugeot: TValue;
}

let select = new GroupedItems<
    Car,
    keyof CarMakers<unknown>
>();

select.addItems([
        { model: 'R5', year: 1980, dummy: false }, // error; correct
        { model: '206', year: 2004 },
        { model: '3008', year: 2010 }
    ],
    car =>
        car.year < 2000
        ? 'audi' // should be an error; "audi" not in "keyof MakerGroups<>"
        : 'peugeot'
);
select.assignGetters({
    renault: () => 'France',
    audi: () => 'Germany' // should be an error; "audi" not in "keyof MakerGroups<>"
});

如您所见,Typescript 没有解析组名,所以我的类型定义不够严格(我想),因此我可以无效地操作不存在的组。从编译时的角度来看,上面的代码似乎很好,但应该为我做检查,intellisense 应该帮助我填写组名。

这是tinker的操场链接。

4

1 回答 1

0

如果您检查select您创建的类型,它是GroupedItems<Car, string | number>. 所以我们可以看到问题出在TGroupKeys. 我们希望它是'renault' | 'peugeot',而不是我们有string | number

您的接口IObjectMap有一个索引签名,这意味着它应该包含任何字符串的值。CarMakersextends每个字符串(以及数字)也是IObjectMap如此keyof CarMakers<unknown>,而不仅仅是对象的特定键。对于每个扩展的对象都是如此IObjectMap

根据打字稿文档

如果您有一个带有字符串索引签名的类型,则 keyof T 将是 string | 数字(不仅仅是字符串,因为在 JavaScript 中,您可以使用字符串(object[“42”])或数字(object[42])来访问对象属性)。

我们需要删除该索引签名。事实上,我们可以在任何地方删除它。

而不是扩展keyof IObjectMap<unknown>,TMapKeys并且TGroupKeys应该扩展string或者PropertyKey- 一个内置类型,它是所有有效属性键 ( string | number | symbol) 的联合。

通过这些更改,我们现在得到了所需的错误:

Type '"audi"' is not assignable to type '"renault" | "peugeot"'
Object literal may only specify known properties, and 'audi' does not exist in type 'Record<"renault" | "peugeot", GetterFunc<Car, string>>'

游乐场链接

于 2021-01-15T23:59:30.167 回答