首先,让我们探讨一下您遇到的类型错误。如果我向 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[]
而所有其他字段都是string
s。我不想用Record<InputFields, string | string[]>
因为这会破坏你对哪些领域的了解string
s 和 哪些是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>