具有混合成员类型的泛型TypeScript接口 [英] Generic TypeScript interface with mixed member types
问题描述
对于多个HTML表单,我想配置输入并处理它们的值。我的类型具有以下结构(您可以将它复制到TypeScript Playground http://www.typescriptlang.org/play 以查看它的行为):
interface InputConfig {
readonly inputType:text 密码| list
只读属性?:ReadonlyArray< string>
readonly cssClasses?:ReadonlyArray< string>
}
接口输入< T扩展InputConfig |字符串|串[]> {
只读用户名:(T& InputConfig)| (T& string)
只读密码:(T& InputConfig)| (T& string)
readonly passwordConfirmation:(T& InputConfig)| (T& string)
readonly firstname:(T& InputConfig)| (T& string)
readonly lastname:(T& InputConfig)| (T& string)
只读兴趣爱好:( T& InputConfig)| (T& string [])
}
type InputConfigs = Inputs< InputConfig> // case 1(见下文)
type InputValues =输入< string |串[]> //情况2(见下文)
const configs:InputConfigs = {
username:{
inputType:text
},
password: {
inputType:password
},
passwordConfirmation:{
inputType:password
},
firstname:{
inputType :text
},
姓氏:{
inputType:text
},
爱好:{
inputType:list
$ b const value:InputValues = {
username:testuser1,
password:test1,
passwordConfirmation:test1 ,
firstname:Tester,
姓氏:1,
爱好:[a,b,c]
}
const username:string = values.username
这种通用方法最重要的优点是 Inputs
接口中的单点命名:名称用户名
,密码 code>,
passwordConfi rmation
,...被两者使用 InputConfigs
和 InputValues
这使重命名变得尽可能简单。
不幸的是,自上次赋值 const username:string = values.username
不被编译器接受:
输入'string | (string& InputConfig)| (string []& InputConfig)| (string []& string)'不能分配给'string'类型。
键入'string []& InputConfig'不能分配给'string'类型。
我预计它可以工作,因为我的理解是:
- 情况1:
T
为InputConfig
,因此username
类型为InputConfig
,因为:
-
T& InputConfig
=InputConfig& InputConfig
=InputConfig
-
T&字符串
=InputConfig&字符串
=无
-
- 情况2:
T
是string |字符串[]
,因此username
的类型为string
,因为:
-
T& InputConfig
=(string | string [])& InputConfig
= nothing -
T&字符串
=(string | string [])&
$ b $首先,我们来探讨一下你得到的类型错误。如果我询问TypeScript的 -
values.username
类型(通过将它悬停在编辑器中),我可以得到 values.username:string
| (string& InputConfig)
| (string []& InputConfig)
| (string []& string)
TypeScript通过实例化输入
的 T
参数到 string |字符串[]
,并将用户名
的类型放入分离标准格式:
((string | string [])& InputConfig)| ((string | string [])& string)
// distribute&在|上
((string& InputConfig)|(string []& InputConfig))| (string& string)|(string []& string))
// reduce(string& string)〜string,置换union操作数
string | (string& InputConfig)| (string []& InputConfig)| (string []& string)
该类型的值可赋值给字符串
?没有! 用户名
可以是 string []& InputConfig
,所以 const x:string = values.username
生病。
将其与您的其他类型进行对比,
configs.username:InputConfig | (InputConfig& string)
此类型可分配给 InputConfig
这是类型系统的一个普遍缺点:它们倾向于拒绝仍然可以在运行时工作的程序。 可能知道 values.username
总是一个字符串
,但你没有' t证明它是满足类型系统,这只是一个愚蠢的机械形式系统。我通常会认为,随着时间的推移,修改类型的程序会更容易理解,并且(更重要的是)更容易继续工作。尽管如此,在像TypeScript这样的高级系统中工作是一种习以为常的技能,很容易陷入盲目的胡同,试图让类型系统按照自己的意愿去做。
我们如何解决这个问题?我对你试图达到的目标有点不清楚,但我将你的问题解释为我如何重复使用各种不同类型的记录的字段名称?这似乎是映射类型的工作。
$ b
映射类型是TypeScript类型系统的一个高级功能,但想法很简单:您将字段名称预先声明为字面类型的联合,然后描述从这些字段名称派生的各种类型。具体来说:
type InputFields =username
| 密码
| passwordConfirmation
| 名字
| 姓氏
| hobbies
现在, InputConfigs
是记录所有这些 InputFields
,每个类型为( readonly
) InputConfig
。 TypeScript库提供了一些
type InputConfigs = Readonly< Record< InputFields,InputConfig>> b
。 html#keyof-and-lookup-typesrel =nofollow noreferrer>
$ blib / lib.d.ts#L1368rel =nofollow noreferrer>
Readonly
和Record
分别定义为:类型Readonly< T> = {
readonly [P in keyof T]:T [P];
}
类型记录< K扩展字符串,T> = {
[P in K]:T;
}
当应用于相关类型时,我们得到:
type InputConfigs = Readonly< Record< InputFields,InputConfig>>
//扩展Record
类型的定义InputConfigs = Readonly< {InputFields中的P:InputConfig; }>
//展开Readonly的定义
type InputConfigs = {
readonly [Q in keyof {[InputFields]:InputConfig}]:{[InputFields中的P:InputConfig} [Q] ;
}
//嵌入式keyof和下标操作符
type InputConfigs = {
只读[InputFields中的Q]:InputConfig;
}
//展开InputFields索引器
type InputConfigs = {
只读用户名:InputConfig;
只读密码:InputConfig;
只读passwordConfirmation:InputConfig;
只读firstname:InputConfig;
readonly lastname:InputConfig;
readonly hobbies:InputConfig;
InputValues
有点棘手,因为它不会以统一的方式将这些字段名映射到类型。hobbies
是一个字符串[]
,而所有其他字段都是string
秒。我不想用Record< InputFields,string |字符串[]>
,因为这抛弃了对哪些字段是string
s以及哪些是string []
秒。 (然后你会遇到与问题中相同的类型错误。)相反,让我们翻转它并将InputValues
作为关于该字段的真实来源名称,并使用 InputFieldskeyof
类型运算符。
type InputValues =只读< {
username:string
password:string
passwordConfirmation:string
firstname:string
姓:字符串
爱好:字符串[]
}>
type InputFields = keyof InputValues
type InputConfigs = Readonly< Record< InputFields,InputConfig>>
现在您的代码类型不加修改地检查。
< pre $const configs:InputConfigs = {
用户名:{
inputType:text
},
密码:{
inputType:password
},
passwordConfirmation:{
inputType:password
},
firstname:{
inputType:text
},
姓氏:{
inputType:text
},
爱好:{
inputType:list
}
$ b $常量值:InputValues = {
用户名:testuser1,
密码:test1,
passwordConfirmation:test1,
名:Tester,
姓:1,
爱好:[a,b,c]
}
usernameConfig:InputConfig = configs.username //很好!
let usernameValue:string = values.username //很棒!
let hobbiesValue:string [] = values.hobbies //更好!For several HTML forms, I want to configure inputs and deal with their values. My types have the following structure (you may copy it to TypeScript Playground http://www.typescriptlang.org/play to see it in action):
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
The most important advantage of this generic approach is the single point of field naming in the
Inputs
interface: The namesusername
,password
,passwordConfirmation
,... are used by bothInputConfigs
andInputValues
which makes renaming as easy as possible.Unfortunately, it does not work as expected since the last assignment
const username: string = values.username
is not accepted by the compiler:Type 'string | (string & InputConfig) | (string[] & InputConfig) | (string[] & string)' is not assignable to type 'string'. Type 'string[] & InputConfig' is not assignable to type 'string'.
I expected it to work because my understanding was:
- case 1:
T
isInputConfig
and thususername
is of typeInputConfig
because:
T & InputConfig
=InputConfig & InputConfig
=InputConfig
T & string
=InputConfig & string
= nothing- case 2:
T
isstring | string[]
and thususername
is of typestring
because:
T & InputConfig
=(string | string[]) & InputConfig
= nothingT & string
=(string | string[]) & string
=string
解决方案First of all, let's explore the type error you're getting. If I ask TypeScript for the type of
values.username
(by hovering over it in an editor) I getvalues.username : string | (string & InputConfig) | (string[] & InputConfig) | (string[] & string)
TypeScript has come up with this type by instantiating
Inputs
'sT
parameter tostring | string[]
and puttingusername
's type into disjunctive normal form:(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)
Is a value of this type assignable to
string
? No!username
could be astring[] & InputConfig
, soconst x : string = values.username
is ill typed.Contrast this with your other type,
configs.username : InputConfig | (InputConfig & string)
This type is assignable to
InputConfig
.This is a shortcoming of type systems in general: they have a tendency to reject programs which nonetheless would work at runtime. You may know that
values.username
will always be astring
, but you haven't proved it to the satisfaction of the type system, which is just a dumb mechanical formal system. I'd generally argue that well typed programs tend to be easier to understand, and (crucially) easier to keep working as you change the code over time. Nonetheless, working within an advanced type system like TypeScript is an acquired skill, and it's easy to get trapped down a blind alley trying to make the type system do what you want.
How can we fix this? I'm a little unclear on what you're trying to achieve, but I'm interpreting your question as "how can I reuse the field names of a record with a variety of different types?" This seems like a job for mapped types.
Mapped types are an advanced feature of TypeScript's type system, but the idea is simple: you declare your field names up front as a union of literal types, and then describe a variety of types derived from those field names. Concretely:
type InputFields = "username" | "password" | "passwordConfirmation" | "firstname" | "lastname" | "hobbies"
Now,
InputConfigs
is a record with all of thoseInputFields
, each typed as a (readonly
)InputConfig
. The TypeScript library provides some useful type combinators for this:type InputConfigs = Readonly<Record<InputFields, InputConfig>>
Readonly
andRecord
are defined respectively as:type Readonly<T> = { readonly [P in keyof T]: T[P]; } type Record<K extends string, T> = { [P in K]: T; }
When applied to the types in question we get:
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
is a bit trickier, because it doesn't map those field names to types in a uniform manner.hobbies
is astring[]
whereas all the other fields arestring
s. I don't wanna useRecord<InputFields, string | string[]>
because that throws away your knowledge of which fields arestring
s and which arestring[]
s. (Then you'd be stuck with the same type error as you had in the question.) Instead, let's flip this around and treatInputValues
as the source of truth about the field names, and deriveInputFields
from it using thekeyof
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>>
Now your code type checks without modification.
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!
这篇关于具有混合成员类型的泛型TypeScript接口的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!