对于多个HTML表单,我想配置输入并处理它们的值。我的类型具有以下结构(您可以将其复制到TypeScript Playground 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
,...由 InputConfigs
和 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'.
我希望它能起作用,因为我的理解是:
T
为InputConfig
,因此username
的类型为InputConfig
,因为:
T & InputConfig
= InputConfig & InputConfig
= InputConfig
T & string
= InputConfig & string
=无T
为string | string[]
,因此username
的类型为string
,因为:
T & InputConfig
= (string | string[]) & InputConfig
=无T & string
= (string | string[]) & string
= string
答案 0 :(得分:3)
首先,让我们探讨一下您遇到的类型错误。如果我向TypeScript询问values.username
的类型(通过在编辑器中将鼠标悬停在上面),我会得到
values.username : string
| (string & InputConfig)
| (string[] & InputConfig)
| (string[] & string)
TypeScript通过将Inputs
的{{1}}参数实例化为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
可能是username
,因此string[] & InputConfig
输入错误。
将此与您的其他类型对比,
const x : string = values.username
此类型可分配给configs.username : InputConfig | (InputConfig & string)
。
这通常是类型系统的一个缺点:它们倾向于拒绝在运行时仍然可以工作的程序。 你可能知道InputConfig
将始终是values.username
,但你还没有证明它对类型系统的满意度,类型系统只是一个愚蠢的机械正式系统。我一般认为,好的类型程序往往更容易理解,并且(关键)更容易继续工作随着时间的推移更改代码。尽管如此,在类似TypeScript的高级类型系统中工作是一项后天的技能,并且很容易陷入盲目的小巷,试图让类型系统做你想做的事。
我们如何解决这个问题?我对你想要达到的目标有点不清楚,但我将你的问题解释为“我如何重用具有各种不同类型的记录的字段名称?”这似乎是mapped types的工作。
映射类型是TypeScript类型系统的高级功能,但这个想法很简单:您将字段名称预先声明为文字类型的并集,然后描述从这些字段名称派生的各种类型。具体地:
string
现在,type InputFields = "username"
| "password"
| "passwordConfirmation"
| "firstname"
| "lastname"
| "hobbies"
是包含所有InputConfigs
的记录,每个记录都为(InputFields
)readonly
。 TypeScript库为此提供了一些useful type combinators:
InputConfig
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;
}
有点棘手,因为它不会以统一的方式将这些字段名称映射到类型。 InputValues
是hobbies
,而所有其他字段都是string[]
。我不想使用string
因为这会让你不知道哪些字段是Record<InputFields, string | string[]>
,哪些字段是string
。 (然后你会遇到与问题中相同的类型错误。)相反,让我们翻一下,将string[]
视为关于字段名称的真相来源,并派生InputValues
使用the keyof
type operator。
InputFields
现在您的代码类型无需修改即可进行检查。
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
等类型}:
InputValues
答案 1 :(得分:1)
每当你有一个工会Foo | Bar
并且想要将其用作Foo
时,你有两个选择:
在您的情况下,您只希望它是string
更改
const username: string = values.username // ERROR
到
const username: string = typeof values.username === 'string' ? values.username : ''; // OKAY
const username: string = values.username as string;
请注意type assertion is you telling the compiler "I know better"。