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

编程入门 行业动态 更新时间:2024-10-21 03:40:29
本文介绍了具有混合成员类型的泛型TypeScript接口的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧! 问题描述

对于多个HTML表单,我想配置输入并处理它们的值。我的类型具有以下结构(您可以将它复制到TypeScript Playground www.typescriptlang/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 []& 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 $ b

      lib / 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 作为关于该字段的真实来源名称,并使用 InputFields 。 html#keyof-and-lookup-typesrel =nofollow noreferrer> keyof 类型运算符。

      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 www.typescriptlang/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 names username, password, passwordConfirmation,... are used by both InputConfigs and InputValues 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 is InputConfig and thus username is of type InputConfig because:
        • T & InputConfig = InputConfig & InputConfig = InputConfig
        • T & string = InputConfig & string = nothing
      • case 2: T is string | string[] and thus username is of type string because:
        • T & InputConfig = (string | string[]) & InputConfig = nothing
        • T & 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 get

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

      TypeScript has come up with this type by instantiating Inputs's T parameter to string | string[] and putting username'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 a string[] & InputConfig, so const 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 a string, 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 those InputFields, each typed as a (readonly) InputConfig. The TypeScript library provides some useful type combinators for this:

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

      Readonly and Record 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 a string[] whereas all the other fields are strings. I don't wanna use Record<InputFields, string | string[]> because that throws away your knowledge of which fields are strings and which are string[]s. (Then you'd be stuck with the same type error as you had in the question.) Instead, let's flip this around and treat InputValues as the source of truth about the field names, and derive InputFields from it using 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>>

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

本文发布于:2023-05-29 23:08:19,感谢您对本站的认可!
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:接口   成员   类型   TypeScript

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!