getConditionalFlowTypeOfType
function getConditionalFlowTypeOfType(type: Type, node: Node) {
let constraints: Type[] | undefined;
let covariant = true;
while (node && !isStatement(node) && node.kind !== SyntaxKind.JSDoc) {
const parent = node.parent;
// only consider variance flipped by parameter locations - `keyof` types would usually be considered variance inverting, but
// often get used in indexed accesses where they behave sortof invariantly, but our checking is lax
if (parent.kind === SyntaxKind.Parameter) {
covariant = !covariant;
}
// Always substitute on type parameters, regardless of variance, since even
// in contravariant positions, they may rely on substituted constraints to be valid
if ((covariant || type.flags & TypeFlags.TypeVariable) && parent.kind === SyntaxKind.ConditionalType && node === (parent as ConditionalTypeNode).trueType) {
const constraint = getImpliedConstraint(type, (parent as ConditionalTypeNode).checkType, (parent as ConditionalTypeNode).extendsType);
if (constraint) {
constraints = append(constraints, constraint);
}
}
// Given a homomorphic mapped type { [K in keyof T]: XXX }, where T is constrained to an array or tuple type, in the
// template type XXX, K has an added constraint of number | `${number}`.
else if (type.flags & TypeFlags.TypeParameter && parent.kind === SyntaxKind.MappedType && !(parent as MappedTypeNode).nameType && node === (parent as MappedTypeNode).type) {
const mappedType = getTypeFromTypeNode(parent as TypeNode) as MappedType;
if (getTypeParameterFromMappedType(mappedType) === getActualTypeVariable(type)) {
const typeParameter = getHomomorphicTypeVariable(mappedType);
if (typeParameter) {
const constraint = getConstraintOfTypeParameter(typeParameter);
if (constraint && everyType(constraint, isArrayOrTupleType)) {
constraints = append(constraints, getUnionType([numberType, numericStringType]));
}
}
}
}
node = parent;
}
return constraints ? getSubstitutionType(type, getIntersectionType(constraints)) : type;
}
getConditionalFlowTypeOfType
函数分析
getConditionalFlowTypeOfType
函数的目的是在 TypeScript 的抽象语法树(AST)中,根据给定的 type
(类型)和 node
(节点)上下文,确定该类型的有效流动类型。它通过向上遍历 AST,检查节点的父节点,追踪变体(协变或逆变)、收集约束,并应用必要的类型替换来实现这一点。该函数在处理条件类型、映射类型和类型参数时尤为重要。
函数目的与核心机制
- 功能:函数接收一个
type
(类型)和一个node
(AST 中的节点),通过从node
向上遍历其父节点,分析语法上下文,判断type
是否受到额外约束或需要替换。 - 输出:返回原始
type
(如果无变化)或基于遍历中收集的约束替换后的类型。 - 关键变量:
constraints
:存储对type
的约束类型数组。covariant
:布尔值,追踪类型是否处于协变(默认true
)或逆变(翻转为false
)位置。
- 终止条件:遍历在遇到语句级节点(如
isStatement(node)
)或 JSDoc 节点时停止,因为这些通常标志着类型流动分析的边界。
变体追踪:参数位置与类型安全
-
为何关注变体:在类型系统中,变体决定了子类型关系如何传播。协变保持子类型方向(例如,
Dog
可赋值给Animal
),而逆变则反转方向(例如,接受Animal
的函数可赋值给接受Dog
的函数)。 -
处理方式:当
node
的父节点是参数(SyntaxKind.Parameter
)时,函数翻转covariant
标志(covariant = !covariant
)。- 原因:函数参数位置是逆变的,因为接受更广类型(如
Animal
)的函数可以安全替代接受更窄类型(如Dog
)的函数,但反之不行。这是为了确保类型安全。
示例:
interface Animal { name: string } interface Dog extends Animal { bark(): void } type AnimalCallback = (a: Animal) => void; type DogCallback = (d: Dog) => void; let feedAnimal: AnimalCallback = (animal) => console.log("喂食:" + animal.name); let feedDog: DogCallback = (dog) => { console.log("喂食:" + dog.name); dog.bark(); }; feedDog = feedAnimal; // 正确:AnimalCallback 更广,适用于 DogCallback // feedAnimal = feedDog; // 错误:DogCallback 要求 bark(),Animal 没有
- 如果允许
feedAnimal = feedDog
,调用feedAnimal({ name: "猫咪" })
会因缺少bark()
而运行时出错。参数位置的变体翻转防止了这种错误。
- 原因:函数参数位置是逆变的,因为接受更广类型(如
-
注释说明:代码指出,虽然
keyof
类型通常导致变体反转,但在索引访问(如T[keyof T]
)中,它表现得更像不变(invariant),TypeScript 的检查在此处较为宽松。
条件类型中的类型参数替换
- 场景:当父节点是条件类型(
SyntaxKind.ConditionalType
),且当前node
是trueType
分支(例如T extends X ? Y : Z
中的Y
)时,函数计算隐含约束。 - 逻辑:
- 如果类型是协变的(
covariant === true
)或类型变量(TypeFlags.TypeVariable
),则调用getImpliedConstraint
,传入条件类型的checkType
(如T
)和extendsType
(如X
)。 - 将得到的
constraint
添加到constraints
数组。
- 如果类型是协变的(
- 为何总是替换类型参数:注释解释,即使在逆变位置,也要替换类型参数,因为它们的有效性可能依赖于替换后的约束。
-
示例: 若不替换,
Test1
中的T
可能失去具体性(例如退化为string
而非"hello"
),破坏类型推断。type Example<T> = T extends string ? T : never; type Test1 = Example<"hello">; // 结果为 "hello" type Test2 = Example<string>; // 结果为 string type Test3 = Example<number>; // 结果为 never
-
- 意义:这确保条件类型能保留精确的类型信息,尤其是对于字面量类型或受约束的类型参数。
同态映射类型的特殊处理
-
场景:当
type
是类型参数(TypeFlags.TypeParameter
),父节点是映射类型(SyntaxKind.MappedType
),且node
是映射类型的type
(如{ [K in keyof T]: XXX }
中的XXX
)时,函数应用特殊约束。 -
逻辑:
- 确认映射类型是同态的(保留
T
的结构)且无nameType
(表示简单的keyof T
映射)。 - 如果
T
被约束为数组或元组类型(通过everyType(constraint, isArrayOrTupleType)
检查),则键类型K
增加约束number | \\
${number}“(数字或数字字符串)。 - 将此约束添加到
constraints
。
示例:
type ArrayKeys<T extends any[]> = { [K in keyof T]: K }; type Result = ArrayKeys<[string, number]>; // { "0": "0", "1": "1" }
- 这里,
K
(键类型)可以是0
或1
(数字),也可以是"0"
或"1"
(数字字符串),反映了 JavaScript 中数组/元组索引的双重性质。
- 确认映射类型是同态的(保留
-
原因:TypeScript 中数组和元组使用数字索引,但在 JavaScript 中,这些索引也可以是字符串(如
arr["0"]
)。number | \\
${number}“ 约束适应了这种特性。
最终类型替换
- 结束方式:遍历完成后:
- 如果
constraints
非空,则计算所有约束的交集(getIntersectionType(constraints)
),并用getSubstitutionType
将其替换到原始type
中。 - 如果无约束,则返回原始
type
。
- 如果
- 结果:返回的类型反映了 AST 结构施加的上下文约束,确保类型检查和推断的准确性。