我有一组存储源数据的SON文件.一种语言有一个文件,所有文件都具有相同的键,但值类型不同.是这样的:

// source-en.json
{
  "action": ["change", "check"],
  "device": ["coil", "relay"],
}
// source-pl.json
{
  "action": ["wymienić", "sprawdzić"],
  "device": [
    {
      "gender": "f",
      "singular": "cewkę",
      "plural": "cewki"
    },
    {
      "gender": "m",
      "singular": "przekaźnik",
      "plural": "przekaźniki"
    }
  ]
}

所以,我可以用界面来描述它们:

interface SourceEn {
  action: string[],
  device: string[],
}

interface SourcePl {
  action: string[],
  device: NounForms[],
}

interface NounForms {
  gender: 'm' | 'f' | 'n' | 'pl';
  singular: string;
  plural: string;
}

现在,我有了消耗数据的一组类:EnConsumerPlConsumer.继承自BaseConsumer.BaseConsumer知道如何获取源文件并将SON转换为实际数据,而具体类知道如何处理特定于语言的差异.所以,BaseConsumer看起来是这样的:

type Source = SourceEn | SourcePl;

class BaseConsumer<T extends Source> {
  public readonly action: Collection<ArrayItem<T['action']>>;
  public readonly device: Collection<ArrayItem<T['device']>>;

  public constructor(language: Language) {
    const data = this.getData(language);
    this.action = new Collection(data.action);
    this.device = new Collection(data.device);
  }
  
  protected getData(language: Language): T {...}
}

class Collection<T> {
  public constructor(private store: T[]) {}
}

type ArrayItem<A extends unknown[]> = A extends (infer E)[] ? E : never;

enum Language {
  EN = 'en',
  PL = 'pl',
}

但它不起作用.具体来说,this.device = new Collection(data.device);行给出两个错误:

  1. data.device:

Argument of type string[] | NounForms[] is not assignable to parameter of type string[]
Type NounForms[] is not assignable to type string[]
Type NounForms is not assignable to type string

  1. this.device:

Type Collection<string> is not assignable to type Collection<Source['device']>
Type string is not assignable to type Source['device']

问题源于T extends Source代表SourceEnSourcePl的联合这一事实.但对于class ConsumerPl extends BaseConsumer<SourcePl>ConsumerPl类应该知道哪些接口实际应用以及哪些类型应该应用于相应的字段.

我不知道如何用TypScript表达它.我不想太明确,因为今天它支持英语和波兰语,但明天也可能是克林贡语和昆亚语.因此,冗长的条件类型不是有效的解决方案.那么如何才能使这BaseConsumer个正确的通用呢?

推荐答案

解决错误的最低更改如下:

type ArrayItem<A extends unknown[]> = A[number];

class BaseConsumer<T extends Source> {
    public readonly action: Collection<ArrayItem<T['action']>>;
    public readonly device: Collection<ArrayItem<T['device']>>;

    public constructor(language: Language) {
        const data = this.getData(language);
        this.action = new Collection<ArrayItem<T['action']>>(data.action);
        this.device = new Collection<ArrayItem<T['device']>>(data.device);
    }

    protected getData(language: Language): T { return null! }
}

我已经将ArrayItem从您的conditional type更改为更简单的indexed access type. 类型Array<X>的数组具有类型{[k: number]: X}的数字index signature,因为TypScript将数组建模为在数字键处具有元素. 因此,如果您有一个string[]并使用数字键对其进行索引,您将得到一个string. 您的条件类型是conceptually相同,但对于编译器来说,通用条件类型比通用索引访问类型更难推理.

我还手动将通用类型参数指定为Collection作为相应的通用类型.如果您不这样做,那么编译器会try infer该类型,最终将dataT扩展到constraint Source,并且不再知道union对应于this.actionthis.device的类型.TypScript将通用值扩展到其约束是一个常见的trap ,避免这种情况的一种方法是手动为函数指定通用类型参数.


这一切都很好,但你得不到BaseConsumer个中所需的通用行为. 由于构造函数参数是非通用的Language,因此您从中得到了无用的BaseConsumer<Source>:

const pl = new BaseConsumer(Language.PL)
//const pl: BaseConsumer<Source>
pl.device
// ^? (property) BaseConsumer<Source>.device: Collection<string | NounForms>

这不是你想要的. pl.device的类型应该是Collection<NounForms>吧? 所以我们需要一种不同的方法.


为了使其发挥作用,我们应该在language 构造函数参数的类型L中设置BaseConsumer<L> generic,然后使用映射类型将这些语言名称连接到相应的actiondevice数据类型,而没有任何数组妨碍. 也就是说,我们可以使用像这样的映射

interface LanguageSourceMap {
    en: {
        action: string,
        device: string,
    },
    pl: {
        action: string,
        device: NounForms,
    }
}

然后,例如,LanguageSourceMap[L]['action']是语言Laction数据类型(使用indexed access types深入挖掘到LanguageSourceMap的键和子键).

一旦我们有了它,我们就可以将BaseConsumer写为:

class BaseConsumer<L extends Language> {
    public readonly action: Collection<LanguageSourceMap[L]['action']>;
    public readonly device: Collection<LanguageSourceMap[L]['device']>;

    public constructor(language: L) {
        const data = this.getData(language);
        this.action = new Collection(data.action);
        this.device = new Collection(data.device);
    }

    protected getData(language: Language):
        { [K in keyof LanguageSourceMap[L]]:
            LanguageSourceMap[L][K][] } { return null! }
}

它工作得很好,因为现在getData()返回mapped type而不是LanguageSourceMap[L],并且用actiondevice索引到that会准确给出我们对this.actionthis.device的索引访问类型. 编译器在L extends LanguageK extends keyof LanguageSourceMap[K]处得出关于LanguageSourceMap[L][K][]的结论比在T extends Source处得出关于LanguageSourceMap[L][K][]的结论要高兴得多. 映射类型和索引访问类型比联合类型和条件类型更容易让编译器推理.请参阅microsoft/TypeScript#47L extends Language了解这种方法的基础,其中我们从LanguageSourceMap等基本映射类型开始,并通过该类型和该类型上的映射类型的通用索引来表达事物.

让我们确保new BaseConsumer人按照我们想要的方式行事:

const pl = new BaseConsumer(Language.PL)
// const pl: BaseConsumer<Language.PL>
pl.device
// ^? (property) BaseConsumer<Language.PL>.device: Collection<NounForms>

看起来不错


我们快完成了.唯一的问题是,当您已经拥有SourceEnSourcePl类型时,我们不想手动写出LanguageSourceMap. 所以让我们重写LanguageSourceMap,以便它显式地消耗这些:

type ToBaseSource<T extends Record<keyof T, any[]>> =
    { [K in keyof T]: T[K][number] };

interface LanguageSourceMap {
    [Language.EN]: ToBaseSource<SourceEn>;
    [Language.PL]: ToBaseSource<SourcePl>;
}

这里,ToBaseSource<T>接受任何属性都是数组的T(它使用一个循环约束,T extends Record<keyof T, any[]>.像X extends Record<keyof X, Y>这样的约束对于约束属性的值类型而不关心键类型是有用的),并且对于每个数组属性T[K],它使用number索引访问来获取数组元素类型.这与简化的ArrayItem映射到t的属性上相同.

现在我们完成了.这里的LanguageSourceMap相当于上面写的手册.


请注意,如果你用startedLanguageSourceMapcomputedSourceEnSourePl来计算,所有这些都会更简单.但事实并非如此,显然你无法控制这些类型.所以这是我能建议的最好的.

Playground link to code

Typescript相关问答推荐

返回签名与其他类型正确的函数不同的函数

在角形加载组件之前无法获取最新令牌

动态判断TypScript对象是否具有不带自定义类型保护的键

处理API响应中的日期对象

使用ngrok解析Angular 时出现HTTP故障

将对象属性转换为InstanceType或原样的强类型方法

根据另一个属性的值来推断属性的类型

有没有办法解决这个问题,类型和IntrinsicAttributes类型没有相同的属性?

TypeScrip:逐个 case Select 退出noUncheck kedIndexedAccess

从TypeScrip中的其他类型检索泛型类型

使用订阅时更新AG网格中的行

有没有可能产生输出T,它在视觉上省略了T中的一些键?

正确使用相交类型的打字集

根据类型脚本中的泛型属性 Select 类型

Typescript 中相同类型的不同结果

如何在Angular 服务中制作同步方法?

棱角分明-什么是...接口类型<;T>;扩展函数{new(...args:any[]):t;}...卑鄙?

字符串文字联合的分布式条件类型

如何处理可能为空的变量...我知道这不是Null?

如何使用 AWS CDK 扩展默认 ALB 控制器策略?