Generics 具有混合成员类型的泛型TypeScript接口

Generics 具有混合成员类型的泛型TypeScript接口,generics,typescript,interface,union,intersection,Generics,Typescript,Interface,Union,Intersection,对于一些HTML表单,我想配置输入并处理它们的值。“我的类型”具有以下结构,您可以将其复制到TypeScript Playerd以查看其运行情况: interface InputConfig { readonly inputType: "text" | "password" | "list" readonly attributes?: ReadonlyArray<string> readonly cssClasses?: ReadonlyArray<st

对于一些HTML表单,我想配置输入并处理它们的值。“我的类型”具有以下结构,您可以将其复制到TypeScript Playerd以查看其运行情况:

interface InputConfig {
    readonly inputType: "text" | "password" | "list"
    readonly attributes?: ReadonlyArray<string>
    readonly cssClasses?: ReadonlyArray<string>
}

interface Inputs<T extends InputConfig | string | string[]> {
    readonly username: (T & InputConfig) | (T & string)
    readonly password: (T & InputConfig) | (T & string)
    readonly passwordConfirmation: (T & InputConfig) | (T & string)
    readonly firstname: (T & InputConfig) | (T & string)
    readonly lastname: (T & InputConfig) | (T & string)
    readonly hobbies: (T & InputConfig) | (T & string[])
}

type InputConfigs = Inputs<InputConfig> // case 1 (see below)
type InputValues = Inputs<string | string[]> // case 2 (see below)

const configs: InputConfigs = {
    username: {
        inputType: "text"
    },
    password: {
        inputType: "password"
    },
    passwordConfirmation: {
        inputType: "password"
    },
    firstname: {
        inputType: "text"
    },
    lastname: {
        inputType: "text"
    },
    hobbies: {
        inputType: "list"
    }
}

const values: InputValues = {
    username: "testuser1",
    password: "test1",
    passwordConfirmation: "test1",
    firstname: "Tester",
    lastname: "1",
    hobbies: ["a", "b", "c"]
}

const username: string = values.username
我希望它能起作用,因为我的理解是:

案例1:T是InputConfig,因此用户名的类型为InputConfig,因为: T&InputConfig=InputConfig&InputConfig=InputConfig T&string=InputConfig&string=nothing 案例2:T是string | string[],因此用户名的类型为string,因为: T&InputConfig=string | string[]&InputConfig=nothing T&string=string | string[]&string=string 当您有一个union Foo | Bar并希望将其用作Foo时,您有两个选项:

使用铅字防护装置 在您的情况下,您只希望它是一个字符串更改

使用类型断言
注意一点。

首先,让我们探讨一下您遇到的类型错误。如果我在编辑器中将鼠标悬停在TypeScript上,询问values.username的类型,我会得到

values.username : string
                | (string & InputConfig)
                | (string[] & InputConfig)
                | (string[] & string)
TypeScript通过将Inputs的T参数实例化为string | string[]并将用户名的类型转换为析取范式来实现此类型:

(T & InputConfig) | (T & string)
// set T ~ (string | string[])
((string | string[]) & InputConfig) | ((string | string[]) & string)
// distribute & over |
((string & InputConfig) | (string[] & InputConfig)) | ((string & string) | (string[] & string))
// reduce (string & string) ~ string, permute union operands
string | (string & InputConfig) | (string[] & InputConfig) | (string[] & string)
此类型的值是否可分配给字符串?不username可以是字符串[]&InputConfig,因此const x:string=values.username类型错误

对比一下你的另一种类型

configs.username : InputConfig | (InputConfig & string)
此类型可分配给InputConfig

这通常是类型系统的一个缺点:它们倾向于拒绝在运行时仍能工作的程序。您可能知道values.username将始终是一个字符串,但您还没有证明它符合类型系统的要求,它只是一个愚蠢的机械形式系统。我通常认为,类型良好的程序往往更容易理解,而且随着时间的推移,随着代码的更改,更容易继续工作。尽管如此,在像TypeScript这样的高级类型系统中工作是一项后天习得的技能,而且很容易陷入死胡同,试图让类型系统实现您想要的功能

我们如何解决这个问题?我有点不清楚您试图实现什么,但我将您的问题解释为如何使用各种不同类型的记录的字段名?这似乎是我的工作

映射类型是TypeScript类型系统的高级功能,但其思想很简单:首先将字段名声明为文本类型的联合,然后描述从这些字段名派生的各种类型。具体地说:

type InputFields = "username"
                 | "password"
                 | "passwordConfirmation"
                 | "firstname"
                 | "lastname"
                 | "hobbies"
现在,InputConfigs是一个包含所有这些InputFields的记录,每个InputFields都被键入为只读InputConfig。TypeScript库为此提供了一些功能:

type InputConfigs = Readonly<Record<InputFields, InputConfig>>
当应用于所讨论的类型时,我们得到:

type InputConfigs = Readonly<Record<InputFields, InputConfig>>
// expand definition of Record
type InputConfigs = Readonly<{ [P in InputFields]: InputConfig; }>
// expand definition of Readonly
type InputConfigs = {
    readonly [Q in keyof { [P in InputFields]: InputConfig }]: { [P in InputFields]: InputConfig }[Q];
}
// inline keyof and subscript operators
type InputConfigs = {
    readonly [Q in InputFields]: InputConfig;
}
// expand InputFields indexer
type InputConfigs = {
    readonly username: InputConfig;
    readonly password: InputConfig;
    readonly passwordConfirmation: InputConfig;
    readonly firstname: InputConfig;
    readonly lastname: InputConfig;
    readonly hobbies: InputConfig;
}
现在,您的代码类型将在不修改的情况下进行检查

常量配置:输入配置={ 用户名:{ 输入类型:文本 }, 密码:{ 输入类型:密码 }, 密码确认:{ 输入类型:密码 }, 名字:{ 输入类型:文本 }, 姓氏:{ 输入类型:文本 }, 爱好:{ 输入类型:列表 } } 常量值:输入值={ 用户名:testuser1, 密码:test1, 密码确认:test1, 名字:测试员, 姓:1,, 爱好:[a、b、c] } 让usernameConfig:InputConfig=configs.username//好! 让usernameValue:string=values.username//太好了!
让hobbiesValue:string[]=values.cabbies//更好!我的类型安全表单/字段状态解决方案:适用于任何地方mobx适用于react/angular/inferno/vue。感谢您的输入。我来看看。MobX在我的观察名单上,因为它是一种很好的状态管理方法。但就目前而言,我更喜欢这个问题所涉及的只有TypeScript的解决方案。类型InputValues本身应该可以做到这一点,而不必多次写入成员名称。我必须说,我觉得这很有趣。这真是一个优雅的解决方案。非常感谢你的帮助和出色的回答。即使我不希望像本案那样有更好的回答,我通常会等到悬赏期结束。
configs.username : InputConfig | (InputConfig & string)
type InputFields = "username"
                 | "password"
                 | "passwordConfirmation"
                 | "firstname"
                 | "lastname"
                 | "hobbies"
type InputConfigs = Readonly<Record<InputFields, InputConfig>>
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
}
type Record<K extends string, T> = {
    [P in K]: T;
}
type InputConfigs = Readonly<Record<InputFields, InputConfig>>
// expand definition of Record
type InputConfigs = Readonly<{ [P in InputFields]: InputConfig; }>
// expand definition of Readonly
type InputConfigs = {
    readonly [Q in keyof { [P in InputFields]: InputConfig }]: { [P in InputFields]: InputConfig }[Q];
}
// inline keyof and subscript operators
type InputConfigs = {
    readonly [Q in InputFields]: InputConfig;
}
// expand InputFields indexer
type InputConfigs = {
    readonly username: InputConfig;
    readonly password: InputConfig;
    readonly passwordConfirmation: InputConfig;
    readonly firstname: InputConfig;
    readonly lastname: InputConfig;
    readonly hobbies: InputConfig;
}
type InputValues = Readonly<{
    username: string
    password: string
    passwordConfirmation: string
    firstname: string
    lastname: string
    hobbies: string[]
}>
type InputFields = keyof InputValues
type InputConfigs = Readonly<Record<InputFields, InputConfig>>