首页 > 解决方案 > 通过将键和关联值类型传递给泛型来缩小索引类型

问题描述

与类似问题的差异

不幸的是,对将隐式键和值类型关系传递给 TypeScript 泛型问题的出色答案并未涵盖当前问题中的问题:在该问题generateInputsAccessObject的目标函数中,我们不使用与子类型相关的属性。

事实上,上面的问题已经解决了,没有将隐式的键和值类型关系传递给 TypeScript 泛型,也就是标题,但我想现在我们不能再避免了。

目标

创建EntitySpecification, 的泛型类型ProductSpecification如:

  1. properties的类型必须是非索引的。这意味着 TypeScript 编译器必须知道存在什么IDprice存在,但其他键 - 不存在。
  2. properties必须是可迭代的(Object.entires("ProductSpecification.properties")必须工作)。
  3. IDandprice可以有不同的类型,但是当我们调用EntitySpecification.properties.IDor时EntitySpecification.properties.price,TypeScript 编译器必须知道是哪种类型。

export type Product = {
  ID: string;
  price: number;
};

const ProductSpecification: EntitySpecification<keyof Product> = {
  name: "Product",
  properties: {
    ID: {
      type: DataTypes.string,
      emptyStringIsAllowed: false
    },
    price: {
      type: DataTypes.number,
      numberSet: NumbersSets.nonNegativeInteger
    }
  }
};

这里:

export enum DataTypes {
  number = "NUMBER",
  string = "STRING"
}


export enum NumbersSets {
  naturalNumber = "NATURAL_NUMBER",
  nonNegativeInteger = "NON_NEGATIVE_INTEGER",
  negativeInteger = "NEGATIVE_INTEGER",
  negativeIntegerOrZero = "NEGATIVE_INTEGER_OR_ZERO",
  anyInteger = "ANY_INTEGER",
  positiveDecimalFraction = "POSITIVE_DECIMAL_FRACTION",
  negativeDecimalFraction = "NEGATIVE_DECIMAL_FRACTION",
  decimalFractionOfAnySign = "DECIMAL_FRACTION_OF_ANY_SIGN",
  anyRealNumber = "ANY_REAL_NUMBER"
}

export type StringSpecification = {
  readonly type: DataTypes.string;
  readonly emptyStringIsAllowed: boolean;
};

export type NumberSpecification = {
  readonly type: DataTypes.number;
  readonly numberSet: NumbersSets;
};

当然,EntitySpecification预先不知道密钥,但是密钥计数可能是任意大的(不是当前的 2 个ProductSpecification)。

现在最好的我

以下解决方案满足前两个目标:

export type EntitySpecification<Keys extends string> = {
  readonly name: string;
  readonly properties: { [key in Keys]: StringSpecification | NumberSpecification };
};

这里的冲突情况,违反第三个条件的后果:

type StringValidationRules = {
  emptyStringIsAllowed: boolean;
};

const ID_ValidationRules: StringValidationRules = {
  emptyStringIsAllowed: ProductSpecification__EXPERIMENTAL.properties.ID.emptyStringIsAllowed;
}

因为 TypeScript 不知道IDStringSpecification类型,所以我们有以下错误:

TS2339: Property 'emptyStringIsAllowed' does not exist on type 'StringSpecification | NumberSpecification'.   Property 'emptyStringIsAllowed' does not exist on type 'NumberSpecification'.

标签: typescript

解决方案


看起来您EntitySpecification只映射对象类型的属性键类型,而您还需要映射相应的值类型。在这种情况下,最好传入整个对象类型而不仅仅是其键。然后,您可以通过一些映射结构映射属性类型,同时像现在一样继续映射键。

这是表示必要映射结构的一种方法:

type DataMapping =
    { type: number, specification: NumberSpecification }
    | { type: string, specification: StringSpecification }

请注意,我不打算在任何地方使用type的实际值。DataMapping它只是一个类型定义,编译器将能够使用它来将属性类型连接到规范接口。

现在EntitySpecification可以定义如下:

type EntitySpecification<T extends Record<keyof T, DataMapping["type"]>> = {
    readonly name: string;
    readonly properties: { [K in keyof T]: Extract<DataMapping, { type: T[K] }>['specification'] };
};

请注意,我被限制 为一个对象类型,其属性T类型在. 这将允许其属性类型仅为and ,但不允许像因为没有(当前)有相应的规范。typeDataMappingProductnumberstring{oops: boolean}boolean

properties属性是一个映射类型,其中的键K与 的键相同T(就像您之前所做的那样),并且其值是从 的相应属性类型转换而来的T,即T[K].

具体的属性映射是Extract<DataMapping, {type: T[K]}>['specification'],使用Extract实用程序类型选择属性为的DataMapping联合体的成员,然后返回该成员的属性。typeT[K]specification


让我们看看它是如何工作的Product

type Product = {
    ID: string;
    price: number;
};

type ProductSpecification = EntitySpecification<Product>;
/* type ProductSpecification = {
    readonly name: string;
    readonly properties: {
        ID: StringSpecification;
        price: NumberSpecification;
    };
} */

这就是你想要的,我想。如果你有一个p类型的值ProductSpecification,那么p.properties.ID必须是类型的StringSpecification,而且p.properties.price必须是类型的NumberSpecification。万岁!


Playground 代码链接


推荐阅读