【Typescript】进击的基础(二)条件类型

614 阅读7分钟

引言

  1. 语法 T extends U ? X : Y。一般语意为:如果 checkType 可赋值给 extendType ,则结果为真分支类型 tureType ,否则结果为假分支类型 falseType
  2. 探讨neverunknownany 等特殊类型的原因及结果
  3. 这系列文章目前打算走推导式风格(应该?),前面定义、概念挺多的,静下来心来慢慢看呗。

类型概念阐述

部分背景如上篇文章所述:【Typescript】进击的基础(一)交叉类型和联合类型-集合论角度理解

这里也会套用部分数学概念,结合实际情况和资料推测typescript的行为。

never

如果使用数学的 集合 概念来描述typescript类型,那么never对应的就是“空集”。套用空集的运算概念,可以得出以下结果:

type A = never | string; // type A = string
type B = never & string; // type A = never
const a: string = '' as never;
const b: never = '' as never;
const c: never = '' as any; // error

其中比较有意思的是最后一点:空集的子集只有空集本身,所以never类型只接受never类型的赋值;而any在TS中是作为类型后门的存在,可赋值给任何类型。这里的问题就出来了,要遵守哪一条设计呢?为什么最后会采用any不可赋值给never这个设计?

个人愚见,实际上never出现的情况基本都是明确告知用户不要使用该类型的变量,或者此处出现错误。所以从安全角度出发,不允许any赋值给never

unknown

对应“全集”概念:所有集合都是全集的子集,所以任意类型的变量都可以赋值给unknown类型。

any的区别就在于unknown仍然是服从整体类型设计的,并不是作为类型后门的存在。比如当你希望一个参数可以接受任意类型时,应当声明为unknown而不是any

关于unknown类型没有任何“属性的类型提示”:同前文思想,unknown类型虽然代表着一个变量可能有任意属性,但这同时意味着一个变量可能没有某些属性。TS无法确保属性是否存在,所以该类型变量的任意属性读写都视作error,也不提供属性提示。

nullundefined

这两个类型受 “strictNullChecks” 选项的影响,开启和关闭该选项有不同的行为:

不启用 strictNullChecks

nullundefined被TS视为可以赋值给任意类型的特殊值,所有非空集合都会包含nullundefined。比如

  • 字面量字符串类型"1",此时它对应的集合元素有3个: "1"nullundefined
  • 类型null包含两个元素:nullundefined
  • 类型undefined包含两个元素:nullundefined

启用 strictNullChecks

nullundefined不再被视为特殊值。

  • 字面量字符串类型"1",此时它对应的集合元素只有1个: "1"
  • 类型null只包含1个元素:null
  • 类型undefined只包含1个元素:undefined

条件类型运算

基于前面的描述,下面的示例套入相应集合概念,大部分特殊值运算都是可以推导得到的(启用strictNullChecks选项):

type A = null extends unknown ? true : false; // true
type B = 1 extends unknown ? true : false; // true
type C = unknown extends 1 ? true : false; // false

Distributive conditional types

总结:如果 checkType联合类型 且是 泛型参数,那么结果等价于各个联合类型各自运算的结果并集,是Typescript有意设计而为之的特性。

即如果 type T = A | B | C ,那么 T extends U ? X : Y 等价于 (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)。注意要求的位置和生效条件:

type T = 1 | 2;

type Result1 = T extends 1 ? true : false; // false,没有触发Distributive特性

type F<E> = E extends 1 ? true : false;
type Result2 = F<T>; // boolean, 即 true | false,触发Distributive特性

type FF<E> = 1 | 2 extends E ? true : false;
type Result3 = FF<1 | 3>; // false,没有触发Distributive特性

Return falseType for a definitely false extends check. We check an instantiations of the two types with type parameters mapped to the wildcard type, the most permissive instantiations possible (the wildcard type is assignable to and from all types). If those are not related, then no instantiations will be and we can just return the false branch type.

避免真假分支类型同时成立

如果不需要该Distributive特性,只想根据条件的真或假来得到结果,那么利用 元组类型 避开 Distributive 特性即可:

type T = 1 | 2;
type F<E> = [E] extends [1] ? true : false;
type Result = F<T>; // false

过滤联合类型

结合空集never的特性,可以提供各类高级玩法,如“过滤”:

type F<T> = T extends string ? T : never
type Result = F<"1" | 2>
            = ("1" extends string ? "1" : never) | (2 extends string ? 2 : never)
            = "1" | never
            = "1"

anyunknown

type D = unknown extends any ? true : false; // true
type E = any extends unknown ? true : false; // true
type F = unknown extends never ? true : false; // false
type G = any extends never ? true : false; // boolean ,即 true | false

anyunknown的组合会相当有趣,判断逻辑顺序和优先度如下:

  1. extendsType anyunknown时,始终返回 trueType
  2. checkTypeany时,始终返回联合类型 trueType | falseType

这段逻辑是直接写入到判断当中的,具体可点击展开:

getConditionalType
function getConditionalType(root: ConditionalRoot, mapper: TypeMapper | undefined): Type {
  let result;
  let extraTypes: Type[] | undefined;
  // We loop here for an immediately nested conditional type in the false position, effectively treating
  // types of the form 'A extends B ? X : C extends D ? Y : E extends F ? Z : ...' as a single construct for
  // purposes of resolution. This means such types aren't subject to the instatiation depth limiter.
  while (true) {
      const checkType = instantiateType(root.checkType, mapper);
      const checkTypeInstantiable = isGenericObjectType(checkType) || isGenericIndexType(checkType);
      const extendsType = instantiateType(root.extendsType, mapper);
      if (checkType === wildcardType || extendsType === wildcardType) {
          return wildcardType;
      }
      let combinedMapper: TypeMapper | undefined;
      if (root.inferTypeParameters) {
          const context = createInferenceContext(root.inferTypeParameters, /*signature*/ undefined, InferenceFlags.None);
          // We skip inference of the possible `infer` types unles the `extendsType` _is_ an infer type
          // if it was, it's trivial to say that extendsType = checkType, however such a pattern is used to
          // "reset" the type being build up during constraint calculation and avoid making an apparently "infinite" constraint
          // so in those cases we refain from performing inference and retain the uninfered type parameter
          if (!checkTypeInstantiable || !some(root.inferTypeParameters, t => t === extendsType)) {
              // We don't want inferences from constraints as they may cause us to eagerly resolve the
              // conditional type instead of deferring resolution. Also, we always want strict function
              // types rules (i.e. proper contravariance) for inferences.
              inferTypes(context.inferences, checkType, extendsType, InferencePriority.NoConstraints | InferencePriority.AlwaysStrict);
          }
          combinedMapper = mergeTypeMappers(mapper, context.mapper);
      }
      // Instantiate the extends type including inferences for 'infer T' type parameters
      const inferredExtendsType = combinedMapper ? instantiateType(root.extendsType, combinedMapper) : extendsType;
      // We attempt to resolve the conditional type only when the check and extends types are non-generic
      if (!checkTypeInstantiable && !isGenericObjectType(inferredExtendsType) && !isGenericIndexType(inferredExtendsType)) {
          // Return falseType for a definitely false extends check. We check an instantiations of the two
          // types with type parameters mapped to the wildcard type, the most permissive instantiations
          // possible (the wildcard type is assignable to and from all types). If those are not related,
          // then no instantiations will be and we can just return the false branch type.
          if (!(inferredExtendsType.flags & TypeFlags.AnyOrUnknown) && (checkType.flags & TypeFlags.Any || !isTypeAssignableTo(getPermissiveInstantiation(checkType), getPermissiveInstantiation(inferredExtendsType)))) {
              // Return union of trueType and falseType for 'any' since it matches anything
              if (checkType.flags & TypeFlags.Any) {
                  (extraTypes || (extraTypes = [])).push(instantiateType(getTypeFromTypeNode(root.node.trueType), combinedMapper || mapper));
              }
              // If falseType is an immediately nested conditional type that isn't distributive or has an
              // identical checkType, switch to that type and loop.
              const falseType = getTypeFromTypeNode(root.node.falseType);
              if (falseType.flags & TypeFlags.Conditional) {
                  const newRoot = (<ConditionalType>falseType).root;
                  if (newRoot.node.parent === root.node && (!newRoot.isDistributive || newRoot.checkType === root.checkType)) {
                      root = newRoot;
                      continue;
                  }
              }
              result = instantiateType(falseType, mapper);
              break;
          }
          // Return trueType for a definitely true extends check. We check instantiations of the two
          // types with type parameters mapped to their restrictive form, i.e. a form of the type parameter
          // that has no constraint. This ensures that, for example, the type
          //   type Foo<T extends { x: any }> = T extends { x: string } ? string : number
          // doesn't immediately resolve to 'string' instead of being deferred.
          if (inferredExtendsType.flags & TypeFlags.AnyOrUnknown || isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(inferredExtendsType))) {
              result = instantiateType(getTypeFromTypeNode(root.node.trueType), combinedMapper || mapper);
              break;
          }
      }
      // Return a deferred type for a check that is neither definitely true nor definitely false
      const erasedCheckType = getActualTypeVariable(checkType);
      result = <ConditionalType>createType(TypeFlags.Conditional);
      result.root = root;
      result.checkType = erasedCheckType;
      result.extendsType = extendsType;
      result.mapper = mapper;
      result.combinedMapper = combinedMapper;
      result.aliasSymbol = root.aliasSymbol;
      result.aliasTypeArguments = instantiateTypes(root.aliasTypeArguments, mapper!); // TODO: GH#18217
      break;
  }
  return extraTypes ? getUnionType(append(extraTypes, result)) : result;
}

第二点具体设计的原因不清楚,目前只在代码中找到一行注释: Return union of trueType and falseType for 'any' since it matches anything

不过看到相关issues有提到的一个有趣的说法:在该情形下,把any视为任意类型的联合类型(即any = 0 | 1 | 2 ....),同时视作泛型通配类型,那么搭配 Distributive 特性后结果就会是 trueType | falseType

infer

TS类型体操的必备技能。推荐:深入理解 TypeScript--infer

目前抓不到什么值得再写一遍的闪亮点,此略了。

参考资料

Conditional Types

Add 'never' typenever 的PR)

Conditional type T extends U ? X : Y is either resolved to X or Y, should not be both (X|Y) (issue)

Question / Suggestion: Behaviour of unknown in distributive conditional type (issue,里面有个图很秀)

最后

👈👈👈👈 各位看官一定懂 👇👇👇👇👇