具有混合成员类型的通用 TypeScript 接口

2024-04-23

对于几个 HTML 表单,我想配置输入并处理它们的值。我的类型具有以下结构(您可以将其复制到 TypeScript Playgroundhttp://www.typescriptlang.org/play http://www.typescriptlang.org/play看看它的实际效果):

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

这种通用方法最重要的优点是字段命名的单点Inputs界面:名称username, password, passwordConfirmation,... 被使用both InputConfigs and InputValues这使得重命名尽可能容易。

不幸的是,自上次分配以来它没有按预期工作const username: string = values.username编译器不接受:

Type 'string | (string & InputConfig) | (string[] & InputConfig) | (string[] & string)' is not assignable to type 'string'.
  Type 'string[] & InputConfig' is not assignable to type 'string'.

我希望它能起作用,因为我的理解是:

  • case 1: T is InputConfig and thus username is of type InputConfig because:
    • T & InputConfig = InputConfig & InputConfig = InputConfig
    • T & string = InputConfig & string=什么都没有
  • case 2: T is string | string[] and thus username is of type string because:
    • T & InputConfig = (string | string[]) & InputConfig=什么都没有
    • T & string = (string | string[]) & string = string

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

values.username : string
                | (string & InputConfig)
                | (string[] & InputConfig)
                | (string[] & string)

TypeScript 通过实例化提出了这种类型Inputs's T参数为string | string[]并把username的类型转换为析取范式:

(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)

该类型的值是否可分配给string? No! username可能是一个string[] & InputConfig, so const x : string = values.username是错误的类型。

将此与您的其他类型进行对比,

configs.username : InputConfig | (InputConfig & string)

该类型可分配给InputConfig.

一般来说,这是类型系统的一个缺点:它们倾向于拒绝在运行时仍然可以工作的程序。You可能知道values.username永远是一个string,但你还没有证明它令类型系统满意,它只是一个愚蠢的机械形式系统。我通常认为,类型良好的程序往往更容易理解,并且(至关重要的是)更容易继续工作当您随着时间的推移更改代码时。尽管如此,在像 TypeScript 这样的高级类型系统中工作是一项后天习得的技能,并且很容易陷入试图让类型系统做你想做的事的死胡同。


我们该如何解决这个问题?我有点不清楚你想要实现什么,但我将你的问题解释为“如何重用具有各种不同类型的记录的字段名称?”这似乎是一份工作映射类型 https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#mapped-types.

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

type InputFields = "username"
                 | "password"
                 | "passwordConfirmation"
                 | "firstname"
                 | "lastname"
                 | "hobbies"

Now, InputConfigs是包含所有这些的记录InputFields,每个输入为 (readonly) InputConfig。 TypeScript 库提供了一些有用的类型组合器 https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#partial-readonly-record-and-pick为了这:

type InputConfigs = Readonly<Record<InputFields, InputConfig>>

Readonly https://github.com/Microsoft/TypeScript/blob/bcf84f49589eb00decd0567efcd6d9eb789d5452/lib/lib.d.ts#L1368 and Record https://github.com/Microsoft/TypeScript/blob/bcf84f49589eb00decd0567efcd6d9eb789d5452/lib/lib.d.ts#L1382分别定义为:

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;
}

InputValues有点棘手,因为它没有以统一的方式将这些字段名称映射到类型。hobbies is a string[]而所有其他字段都是strings。我不想用Record<InputFields, string | string[]>因为这会破坏你对哪些领域的了解strings 和 哪些是string[]s。 (然后你就会遇到与问题中相同的类型错误。)相反,让我们翻转一下并处理InputValues作为字段名称的真实来源,并得出InputFields从它使用the keyof类型运算符 https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#keyof-and-lookup-types.

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>>

现在您的代码类型检查无需修改。

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"]
}

let usernameConfig: InputConfig = configs.username  // good!
let usernameValue: string = values.username  // great!
let hobbiesValue: string[] = values.hobbies  // even better! ????

如果您有很多这些类型并且想要将它们全部映射到配置类型,我什至会定义一个泛型类型组合器来派生类型,例如InputConfigs from InputValues:

type Config<T> = Readonly<Record<keyof T, InputConfig>>

type InputConfigs = Config<InputValues>
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

具有混合成员类型的通用 TypeScript 接口 的相关文章

随机推荐