递归泛型类型扩展类型-奇怪的行为,这是一个错误吗?

时间:2018-12-05 17:58:07

标签: typescript generics typescript-generics typescript-types

提供界面

interface Test {
  inner: {
    value: boolean,
  }
}

和一个班级

class ContextualData<T> {
   constructor(public data: T) {}
}

我希望能够这样做:

const original: Test = {
  inner: {
    value: true,
  },
}

// Wrap the value in a ContextualData object.
original.inner.value = new ContextualData<boolean>(original.inner.value)

我试图通过声明以下类型来实现:

export type Primitive = undefined | null | boolean | string | number | Function

export type Contextuable<T> = T | ContextualData<T>

export type DeepContextuable<T> =
  T extends Primitive ? Contextuable<T> : DeepContextuableObject<T>

export type DeepContextuableObject<T> = {
  [K in keyof T]: DeepContextuable<T[K]>
}

,然后使用DeepContextual转换我的Test界面:

const original: DeepContextual<Test> = {
  inner: {
    value: new ContextualData<boolean>(true),
  },
}

这很好。

现在,让我们向ContextualData类添加另一个方法:

class ContextualData<T> {
   constructor(public data: T) {}

   public map<U>(mapFn: (current: T) => U): U {
     return mapFn(this.data)
   }
}

即使不使用新功能,带有value: ContextualData<boolean>(true)的上下文接口现在也会引发以下TS错误:

TS2322: Type 'ContextualData<boolean>' is not assignable to type 
'boolean | ContextualData<true> | ContextualData<false>'.

我想念什么?这是错误吗?

1 个答案:

答案 0 :(得分:1)

您遇到了条件类型的分布行为。这种(非常有用的)行为指示条件类型分布在裸类型参数的联合成员上。将其与Typescript将boolean视为true | false的事实配对,我们得到了。

DeepContextuable<boolean> = DeepContextuable<true | false>  
   = DeepContextuable<true> | DeepContextuable<false>  
   = (true | Contextuable<true>) | (false | Contextuable<false>)
   = boolean | Contextuable<true> |  Contextuable<false

此行为仅在裸类型参数上发生。要禁用它,我们可以将参数放在一个元组中,一切将按您期望的那样进行。

export type Primitive = undefined | null | boolean | string | number | Function

export type Contextuable<T> = T | ContextualData<T>

export type DeepContextuable<T> =
[T] extends [Primitive] ? Contextuable<T> : DeepContextuableObject<T>

export type DeepContextuableObject<T> = {
    [K in keyof T]: DeepContextuable<T[K]>
}

interface Test {
    inner: {
        value: boolean,
    }
}

class ContextualData<T> {
    constructor(public data: T) { }

    public map<U>(mapFn: (current: T) => U): U {
        return mapFn(this.data)
    }
}

const original: DeepContextuable<Test> = {
    inner: {
        value: new ContextualData<boolean>(true),
    },
}