typescript 使用接口的属性作为输入来评估过程类型

toe95027  于 2023-01-27  发布在  TypeScript
关注(0)|答案(1)|浏览(94)

我想我知道为什么下面的代码编译失败了(playground):

type Type = [unknown] & { type: unknown };

interface $Zeros extends Type {
//        ~~~~~~
// Type instantiation is excessively deep...
    type: Zeros<this[0]>
}

type Zeros<N, R extends unknown[] = []> =
    N extends R['length'] ? R : Zeros<N, [0, ...R]>;

问题是this[0]就是unknown,所以TS试图扩展Zeros<unknown>,由于我在Zeros中表达条件的方式,它无限地重复。
这是我的假设,因为翻转条件句可以化解它(playground):

// no error with this implementation
type Zeros<N, R extends unknown[] = []> =
    R['length'] extends N ? R : Zeros<N, [0, ...R]>;

但是,当我将Type的实现替换为以下实现时,我不再遇到此错误:

type Type = { 0: unknown, type: unknown }

我甚至可以毫无问题地直接查询该值:

type GetZeros = ($Zeros & [3])['type'];

然而,如果我在一个类型中提取这个逻辑,那么它就不会再编译了(而且在Zeros中翻转条件的方法是相同的):

type apply<$T extends Type, Args> = ($T & Args)['type'];

type applyZeros = apply<$Zeros, [3]>;
//                ~~~~~~~~~~~~~~~~~~
// Type instantiation is excessively deep...

(上述设置的Playground)
我对最后一个片段的行为同样感到惊讶:我期望this[0]3,所以Zeros应该被输入3N extends R['length']应该是3 extends R['length'],所以不应该有无限递归...
很明显,我的思维模式在这个例子中有两个明显的地方失败了。我需要一些洞察力。到底发生了什么?

添加一些上下文

以上是我在library上实验的两个设计的简化版本。{ 0: unknown, type: unknown }实现有很多优点,也是我目前正在使用的一个,但我更喜欢[unknown] & { type: unknown }的行为,因为它可以帮助用户更容易、更快地找到错误的原因。
在库的正常使用过程中,即使没有应用“参数”,$Zeros['type']也应该被其他类型查询(以检查它是否可以组合),因此this[0]unknown(或某些类型约束)的情况以及诸如无限递归或无用/退化返回类型等潜在问题由实现者处理是很重要的。

gajydyqb

gajydyqb1#

好吧,贝娄是一个解释性的模型,它不是经典,但对我来说足够好了,我很乐意被反驳。

Type为元组时的过早求值

在下面的代码中,不计算接口$Zeros中的type字段

type Type = { 0: unknown, type: unknown }

interface $Zeros extends Type {
    type: Zeros<this[0]>
}

但是,它将使用以下单个变更进行评估:

type Type = { [x: number]: unknown, 0: unknown, type: unknown }

交集类型[unknown] & { type: unknown }导致添加索引签名,如下面的代码所示:

type Type = Compute<[unknown] & { type: unknown }>
type Compute<T> = { [K in keyof T]: T[K] };

基本原理是,当接口包含索引签名时,类型检查器无法对对象上存在的属性做出假设,因此在$Zeros的情况下,它无法确定字段0是否存在或其类型是什么,即使键0显式地存在于Type的定义中。因此,TS必须评估type字段以确保它是格式良好的。
尝试演示这一点的一种方法是使用实用程序类型移除索引签名

type Type = RemoveIndex<Compute<[unknown] & { type: unknown }>>
type Compute<T> = { [K in keyof T]: T[K] };
type RemoveIndex<T> = {
  [ K in keyof T as string extends K ? never : number extends K ? never : K ] : T[K]
};

那么我们需要稍微修改一下$Zeros的定义,因为如果没有索引签名,数组只能用字符串键来索引:

interface $Zeros extends Type {
    type: Zeros<this['0']>;
}

上述情况不会触发无限递归错误。

使用实用程序类型apply时的错误

type apply<$T extends Type, Args> = ($T & Args)['type'];

type applyZeros = apply<$Zeros, [3]>;
//                ~~~~~~~~~~~~~~~~~~
// Type instantiation is excessively deep...

可以观察到,通过将鼠标悬停在操场中的applyZeros上可以推断出正确的返回类型([0, 0, 0]),因此触发错误的不是推理引擎。
罪魁祸首是类型检查器,因为它继续用$T = $Zeros检查$T extends Type
实际上,apply的以下实现没有出现这个问题:

// no type constraint
type apply<$T, Args> = ($T & Args)['type' & keyof $T];

// shallow type constraint checking only the keys
type apply<$T extends (keyof $T extends keyof Type ? unknown : never), Args> =
    ($T & Args)['type' & keyof $T];

我的理解是,类型约束$T extends { type: unknown }导致类型检查器将Zeros<this[0]>计算为Zeros<unknown>,这导致无限递归。
我认为这个求值是不必要的,因为T extends unknown总是真的,并且可以跳过总是真的检查,而不是求值类型,我认为简单地检查键是否存在就足够了。
我提交了一份bug report

相关问题