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

时间:2017-05-24 21:19:29

标签: generics typescript interface union intersection

对于多个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界面中的单点域命名:名称usernamepasswordpasswordConfirmation,...由 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:TInputConfig,因此username的类型为InputConfig,因为:
    • T & InputConfig = InputConfig & InputConfig = InputConfig
    • T & string = InputConfig & string =无
  • 案例2:Tstring | string[],因此username的类型为string,因为:
    • T & InputConfig = (string | string[]) & InputConfig =无
    • T & string = (string | string[]) & string = string

2 个答案:

答案 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的记录,每个记录都为(InputFieldsreadonly。 TypeScript库为此提供了一些useful type combinators

InputConfig

ReadonlyRecord分别定义为:

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; } 有点棘手,因为它不会以统一的方式将这些字段名称映射到类型。 InputValueshobbies,而所有其他字段都是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"