ts的奇怪现象合集(一)

238 阅读3分钟

奇怪现象1(双变)

interface type1 {
  doSomething (data: object): 1;
}

interface type2 {
  doSomething: (data: object) => 1;
}

function fn (data: { type: unknown; }): 1 {
    return 1;
}

const A: type1 = {
    doSomething: fn
};
const B: type2 = {
    doSomething: fn 
  //  Error 
  // Type '(data: { type: unknown; }) => boolean' is not assignable to type '(data: object) => boolean'.
};

要弄清楚这个问题需要了解两个知识点:

第一

TypeScript 类型系统有着自下而上的层级,例如原始类型、联合类型、对象类型、内置类型的层级关系。函数类型也有层级关系,判断函数类型的层级关系有相关的规则,这个规则就是双变(协变和逆变)(本文的重点不在于此,需要了解更多的可以自行搜索)

  • 协变:正向的父子类型关系

  • 逆变:反向的父子类型关系

而函数的特性就是双变,利用这个特性,我们最常见的体操姿势:UnionToIntersection

type UnionToIntersection<U> = (U extends any(k: U) => voidneverextends ((k: infer I) => void)
  ? I
  : 1213
  
type TestUnionToIntersection<{ astring } | { bnumbervstring } | { mnumber }>

利用函数的抗变, infer I 的代表的是 A | B, 由于抗变,成了 A&B.

第二

interface type1 {
  doSomething(): void; // 声明了type1包含doSomething这个方法
}

interface type2 {
  doSomething: () => void; // 声明了type2,doSomething作为独立声明的函数。绑定在了type2上
}

在上述代码中

  • ts中的方法函数通过逆变的方式进行检查,而函数会通过双变的形式进行检查。所以type1方法通过逆变的形式检查,type2通过函数的双变检查。导致B赋值给type2会报错
  • type1 声明了type1包含doSomething这个方法
  • type2: 声明了type2,doSomething作为独立声明的函数。绑定在了type2上
  • 很像js中的this指向问题,看起来只差一点,但是实际代表的含义完全不一样.
obj = {a: () => console.log(this)} // window
obj = {a(){ console.log(this) }} // obj
  • 换个方法解释
interface type1 {
  (): 1; // 相当于你声明了type1函数
}
const ex1: type1 = () => 1


interface type2 {
  () => 1; // 报错 ':' expected, 因为这是个声明函数的语法,函数需要有函数名
}

附带资料:ts官方文档对该能力区分的介绍

奇怪现象2(nominal typing)

引用一段vue3的源代码类型声明 源码链接 image.png

在工作中会发现,通过ref声明的对象可以保证不会识别紊乱(读到了类似的结构,导致ts异常),为了弄清楚原因,再次读了下ref的源代码,实验如下:源代码链接

declare const RefSymbol: unique symbol
export interface Ref<T = any> {
  value: T
  /**
   * Type differentiator only.
   * We need this to be in public d.ts but don't want it to show up in IDE
   * autocomplete, so we use a private Symbol instead.
   */
  [RefSymbol]: true
}

// 实验组1
declare const fakeSymbol: unique symbol
type FakeRef<T> = {
  value: T
  [fakeSymbol]: true
}
type FakeInstance = FakeRef<{}>


// 实验组2
interface MaybeRef<T = any> {
  value: T
  [RefSymbol]: true
}

type isExtend1 = FakeInstance extends Ref<{ $data: 1 }> ? true : false; // false
type isExtend2 = MaybeRef<{}> extends Ref<{}> ? true : false; // true

如上述代码,由于RefSymbol没有被导出,所以没办法在外部直接生成一个统一类型的对象,从而保证了ref的类型安全。(并且该方法不会被ide识别出来,现实ref下的Symbol key)

由于ref是一个对象,所以我们完成类似的需求可以直接参考vue的方案,那么基础类型我们应该怎么解决呢?直接上代码

type Fish = string & { __private: 'FISH' }
type Fruit = 'Fruit'

const getFish = () => 'fish' as Fish

const eatFishError = (food: string) => {
  console.log(food);
}
const eatFishSuccess = (food: Fish) => {
  console.log(food);
}


eatFishSuccess(getFish()) // 成功
eatFishError('Fruit') // 没报错
eatFishError(getFish()) // 没报错
eatFishSuccess('Fruit')
// Argument of type 'string' is not assignable to parameter of type 'Fish'.
  // Type 'string' is not assignable to type '{ __private: "FISH"; }'.

在上述代码中

  • Fish,包含了string的所有属性,好比我们js中的String和string的关系。所以想要保证我们的入参有固定的输入来源,通过交叉类型是很有效的解决方案
  • 我们的eatFishError中,由于无法正确区分string和Fish的区别,所以,无论是输入什么都不会返回异常。这对于我们一些重要的类型保护是尤为重要的。
  • 上述的ts特性叫做nominal typing,官方playground

奇怪现象3 Type instantiation is excessively deep and possibly infinite

众所周知,ts的循环次数是有限的,在3.x的版本中循环次数是8(印象中),4.x中循环限制放宽了很多,但是依旧很低(印象中50次?)

我近期在解决LengthOfString问题遇到阻塞(ts认为我的代码无限循环) 后查阅资料,看到了别人家的代码(支持位数更多的循环)playground链接

type LengthOfStringBasic<S extends string> = (S extends "" ? [] :
  S extends `${infer A}${infer B}${infer C}${infer D}${infer E}${infer E}${infer G}${infer H}${infer I}` ? [1, 1, 1, 1, 1, 1, 1, 1, ...LengthOfStringBasic<I>] :
  S extends `${infer A}${infer B}${infer C}${infer D}${infer E}${infer E}${infer G}${infer H}` ? [1, 1, 1, 1, 1, 1, 1, ...LengthOfStringBasic<H>] :
  S extends `${infer A}${infer B}${infer C}${infer D}${infer E}` ? [1, 1, 1, 1, ...LengthOfStringBasic<E>] :
  S extends `${infer A}${infer B}` ? [1, ...LengthOfStringBasic<B>] : never);

type LengthOfString<S extends string> = LengthOfStringBasic<S>['length'];

type sadas = LengthOfString<'12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890'>
  • 没有太多可解释的,很简单粗暴,有迹可循就人工干预,多级区分计算(PS:虽说投机,但是似乎是唯一的解决方案?)