奇怪现象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) => void
: never) extends ((k: infer I) => void)
? I
: 1213
type Test = UnionToIntersection<{ a: string } | { b: number, v: string } | { m: number }>
利用函数的抗变, 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的源代码类型声明 源码链接
在工作中会发现,通过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:虽说投机,但是似乎是唯一的解决方案?)