具有混合成员类型的通用 TypeScript 接口
Generic TypeScript interface with mixed member types
对于几个 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'.
我预计它会起作用,因为我的理解是:
- 情况 1:
T
是 InputConfig
,因此 username
是 InputConfig
类型,因为:
T & InputConfig
= InputConfig & InputConfig
= InputConfig
T & string
= InputConfig & string
= 无
- 情况 2:
T
是 string | string[]
,因此 username
是 string
类型,因为:
T & InputConfig
= (string | string[]) & InputConfig
= 无
T & string
= (string | string[]) & string
= string
每当你有一个联合 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"。
首先,让我们探讨一下您遇到的类型错误。如果我向 TypeScript 询问 values.username
的类型(通过在编辑器中将鼠标悬停在它上面),我得到
values.username : string
| (string & InputConfig)
| (string[] & InputConfig)
| (string[] & string)
TypeScript 通过将 Inputs
的 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
永远是一个string
,但你还没有证明它对类型系统满意,这只是一个愚蠢的机械正式系统。我通常认为,类型良好的程序往往更容易理解,并且(至关重要的)更容易 继续工作 随着时间的推移更改代码。尽管如此,在像 TypeScript 这样的高级类型系统中工作是一项后天技能,很容易陷入死胡同,试图让类型系统做你想做的事。
我们如何解决这个问题?我不太清楚您要实现的目标,但我将您的问题解释为 "how can I reuse the field names of a record with a variety of different types?" 这似乎是 mapped types.
的工作
映射类型是 TypeScript 类型系统的一项高级功能,但其思想很简单:您预先将字段名称声明为文字类型的联合,然后描述从这些字段名称派生的各种类型。具体来说:
type InputFields = "username"
| "password"
| "passwordConfirmation"
| "firstname"
| "lastname"
| "hobbies"
现在,InputConfigs
是一条包含所有这些 InputFields
的记录,每个记录都键入为 (readonly
) InputConfig
。 TypeScript 库为此提供了一些 useful type combinators:
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
视为字段名称的真实来源,并派生 InputFields
从它使用 the keyof
type operator.
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
这样的类型38=]:
type Config<T> = Readonly<Record<keyof T, InputConfig>>
type InputConfigs = Config<InputValues>
对于几个 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'.
我预计它会起作用,因为我的理解是:
- 情况 1:
T
是InputConfig
,因此username
是InputConfig
类型,因为:T & InputConfig
=InputConfig & InputConfig
=InputConfig
T & string
=InputConfig & string
= 无
- 情况 2:
T
是string | string[]
,因此username
是string
类型,因为:T & InputConfig
=(string | string[]) & InputConfig
= 无T & string
=(string | string[]) & string
=string
每当你有一个联合 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"。
首先,让我们探讨一下您遇到的类型错误。如果我向 TypeScript 询问 values.username
的类型(通过在编辑器中将鼠标悬停在它上面),我得到
values.username : string
| (string & InputConfig)
| (string[] & InputConfig)
| (string[] & string)
TypeScript 通过将 Inputs
的 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
永远是一个string
,但你还没有证明它对类型系统满意,这只是一个愚蠢的机械正式系统。我通常认为,类型良好的程序往往更容易理解,并且(至关重要的)更容易 继续工作 随着时间的推移更改代码。尽管如此,在像 TypeScript 这样的高级类型系统中工作是一项后天技能,很容易陷入死胡同,试图让类型系统做你想做的事。
我们如何解决这个问题?我不太清楚您要实现的目标,但我将您的问题解释为 "how can I reuse the field names of a record with a variety of different types?" 这似乎是 mapped types.
的工作映射类型是 TypeScript 类型系统的一项高级功能,但其思想很简单:您预先将字段名称声明为文字类型的联合,然后描述从这些字段名称派生的各种类型。具体来说:
type InputFields = "username"
| "password"
| "passwordConfirmation"
| "firstname"
| "lastname"
| "hobbies"
现在,InputConfigs
是一条包含所有这些 InputFields
的记录,每个记录都键入为 (readonly
) InputConfig
。 TypeScript 库为此提供了一些 useful type combinators:
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
视为字段名称的真实来源,并派生 InputFields
从它使用 the keyof
type operator.
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
这样的类型38=]:
type Config<T> = Readonly<Record<keyof T, InputConfig>>
type InputConfigs = Config<InputValues>