【问题标题】:Typescript: weird behavior on implementation of union of types打字稿:实现类型联合的奇怪行为
【发布时间】:2021-12-27 20:16:54
【问题描述】:

假设我有这个代码。

interface StateType<ExtendedBooleanType extends boolean, ExtendedLogType extends "is true" | "is false" | "both"> {
    bool: ExtendedBooleanType;
    log: ExtendedLogType,
}

type State =
    | StateType<true, "is true">
    | StateType<false, "is false">


const state: State = {
    bool: true,
    log: "is true"
}; // This runs and is expected, great!

const state: State = {
    bool: false,
    log: "is true"
}; // This doesn't run, and is expected, great!

但是当我添加这段代码时,这不起作用。

type StateList =
    | StateType<true, "is true">[]
    | StateType<false, "is false">[]
    | StateType<true & false, "both">[]

const state_list: StateList = [
    { bool: true, log: "both" },
    { bool: false, log: "both" },
]; // This doesn't work, and I don't know why :T

理想情况下,StateList 会识别该列表同时具有 truefalse,因此日志应该是“两者”,但我无法让它工作。

我想让编译器实现 3 种情况,当列表只有 true 时,当列表只有 false 时,当列表两者都有时,并使该类型类似于 @ 987654328@.

有没有办法让它工作?

【问题讨论】:

  • 请记住,真假交集给你never。你想达到什么目标?
  • 我想让编译器意识到列表有真有假,或者只有假,或者只有真。
  • 如果您使用true | false 而不是true &amp; false 会发生什么?正如@captain-yossarian 所说,true &amp; false 变成了never,因为某事不可能同时为真和假。
  • false | true 的问题在于,当它们都是 false 或它们都是 true "both" 时,仍然是 log 的可能值,但这不应该是案子。

标签: typescript


【解决方案1】:

true &amp; false - 给你never。类型不能是truefalse。想象一下可以是0 和同时是1 的数字。这是不可能的。

StateList 类型不应是联合类型。它应该是条件类型。您应该将其视为一个检查列表是否同时包含truefalse 的函数。

此外,StateType 类型允许您创建无效状态 StateType&lt;true, "is false"&gt;,这并不酷。

如果我是你,我会创建一个BooleanMap

type BooleanMap = {
    true: true,
    false: false
}

现在,我们可以遍历地图的每个键并创建适当的状态。

type Values<T> = T[keyof T]

type BooleanState<
    T extends Record<string, boolean>
    > = Values<{
        [Prop in keyof T]: {
            bool: T[Prop],
            log: `is ${Prop & string}`
        }
    }>

// type State = {
//     bool: true;
//     log: "is true";
// } | {
//     bool: false;
//     log: "is false";
// }
type State = BooleanState<BooleanMap>

结果和你的一样。 好的,我知道这是一个有点冗长的解决方案。只是想向您展示这种技术,因为有时使非法状态无法表示非常有用。我在几个答案中使用了这种模式。见this

这个解决方案不那么冗长,并且使用了distributive-conditional-types 的力量:

type MakeState<Bool extends boolean> =
    Bool extends true
    ? { bool: true, log: 'is true' }
    : { bool: false, log: 'is false' }

type State = MakeState<boolean>

如果你想表达false &amp; true === 'both',你不应该把它作为联合的一部分。应该在验证函数/类型中完成。

使用StateType&lt;true &amp; false, "both"&gt;[] 作为列表状态的一部分有点棘手,因为这种类型应该代表我们验证函数的结果。我们应该经过一些计算得到它。如果不使用条件类型,这种类型不能在 TypeScript 中自我表示。

为了验证state_list,我们需要额外的功能。 让我们编写一个函数并尝试推断参数的文字类型:

const validation = <
    Elem extends State,
    Tuple extends Elem[]
>(tuple: [...Tuple]) => tuple

validation([
    { bool: true, log: "is true" },
    { bool: true, log: "is true" },
])

如果您将鼠标悬停在validation 上,您将看到提供的参数已被推断出来。如果您对推理感兴趣,可以查看我的article。如果您想知道 [...Tuple] 的语法是什么意思 - 它是 variadic tuples

好的,让我们继续。现在,当我们推断出元组中的每个元素时,我们可以编写Validate 类型。如果您对 typescript 中函数参数的类型验证感兴趣,可以查看我的文章:thisthis

type TupleMap<T extends State[],Result extends any[]=[]>=
    T extends [] 
    ? Result
    : T extends [infer Head,...infer Tail]
    ? Head extends State
    ? Tail extends State[]
    ? TupleMap<Tail,[...Result,{bool:Head['bool'],log:'both'}]>
    : never
    :never
    :never

type Validation<Tuple extends any[]> =
    boolean extends Tuple[number]['bool']
    ? TupleMap<Tuple>
    : Tuple

Validation 需要 State 的元组。 boolean extends Tuple[number]['bool'] - 表示如果元组中每个元素的属性booltrue | false 的联合,我们需要删除log 属性并添加另一个具有“both”值的属性。为了做到这一点,我们需要递归地遍历元组中的每个元素并覆盖log 属性。 Hereherehere 你会发现更多关于元组迭代的解释。

让我们在函数内部使用Validation

type MakeState<Bool extends boolean> =
    Bool extends true
    ? { bool: true, log: 'is true' }
    : { bool: false, log: 'is false' }

type State = MakeState<boolean>

type EdgeCaseState = { bool: boolean, log: 'both' }


type TupleMap<T extends State[],Result extends any[]=[]>=
    T extends [] 
    ? Result
    : T extends [infer Head,...infer Tail]
    ? Head extends State
    ? Tail extends State[]
    ? TupleMap<Tail,[...Result,{bool:Head['bool'],log:'both'}]>
    : never
    :never
    :never

type Validation<Tuple extends any[]> =
    boolean extends Tuple[number]['bool']
    ? TupleMap<Tuple>
    : Tuple

const validation = <
    Elem extends State,
    Tuple extends Elem[]
>(tuple: Validation<[...Tuple]>) => tuple

validation([
    { bool: true, log: "is true" },
    { bool: true, log: "is true" },
]) // no errors

validation([
    { bool: false, log: "is false" }, // expected error
    { bool: true, log: "is true" }, // expected error
])

validation([
    { bool: false, log: "both" }, // ok
    { bool: true, log: "both" }, // ok
]) 

Playground

TupleMap 是一个有点冗长的实用程序。可以重构它并使用更简洁的版本:


type TupleMap<T extends State[]> = {
    [Prop in keyof T]: T[Prop] extends State ? { bool: T[Prop]['bool'], log: 'both' } : never
}

我知道你要问什么:

问:是否可以去掉多余的功能?

答:没有。请参阅this 文章。我们需要额外的函数来进行验证。

问:额外的函数会影响代码性能吗?

答:请看this的回答。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-10-28
    • 2020-08-23
    • 2017-05-20
    • 1970-01-01
    • 2018-12-06
    • 2017-09-16
    • 2017-05-18
    相关资源
    最近更新 更多