关于typescript:TypeScript老手也容易迷惑的地方

5次阅读

共计 4324 个字符,预计需要花费 11 分钟才能阅读完成。

首先须要阐明下,因为 TypeScript 的类型零碎最终是服务于 JavaScript 的,所以任何 js 写进去的代码,ts 都必须能申明出对应的类型束缚,这就导致 ts 可能会呈现非常复杂的类型申明。而个别的强类型语言则没这样的问题,因为在一开始设计之初,那些无奈用类型零碎申明进去的接口压根就不容许创立。

并且,TypeScript 是结构化类型,有别于名义类型,任何类型是否符合取决于它的构造是否符合,而名义类型则是必须有严格的类型对应。

上面很多内容都是基于下面两点去思考的,上面进入正题。

一、对于枚举

除了失常枚举的申明,在面对不同场景时,枚举申明也会有一些不同。

  1. 动静枚举

容许在枚举中初始化动静的数值,但字符串不行

// 动静数值
enum A {Error = Math.random(),
        Yes = 3 * 9
}

// 当带有字符串时则不能够
enum A {Error = Math.random(), // Error:含字符串值成员的枚举中不容许应用计算值
    Yes = 'Yes',
}
  1. 加 const 前缀
// 枚举加 const 前缀,将会在编译器间接用字符串代替变量援用
const enum NoYes {No='No', Yes='Yes'}

// 编译前:
const a = NoYes.NO
// 编译后:
const a = 'No'
  1. 作为对象
// 因为 ts 是结构性类型零碎,所以枚举也能够作为对象传入,但总感觉怪怪的
enum NoYes {
  No = 'No',
  Yes = 'Yes',
}
function func(obj: { No: string}) {return obj.No;}
func(NoYes); // 编译通过
  1. 数字和字符串枚举的查看宽松度不同
// 数字枚举的宽松查看
enum NoYes {No, Yes}
function func(noYes: NoYes) {}
func(33); // 并不会报类型谬误!// 字符串枚举却报错
enum NoYes {No='No', Yes='Yes'}
function func(noYes: NoYes) {}
func('NO'); // Error: 类型“"NO"”的参数不能赋给类型“NoYes”的参数

之所以容许数字随便赋值给枚举,我猜也是因为容许动静数值枚举的关系。

二、重载为什么不能离开写

ts 中的函数重载:

function foo(p: string);
function foo(p: number);
function foo(p: string | number) {...};

java 中的重载:

public class Overloading {public int foo(){System.out.println("test1");
        return 1;
    }
 
    public void foo(int a){System.out.println("test2");
    }   
}

不反对离开写重载的起因是:

  • 传统的重载是在编译时将重载函数拆分命名(func 拆分为 func1、func2),再在调用处批改命名,从而达到通过参数辨别调用的成果。而 JavaScript 在运行时能够随时批改类型,如果仍然采纳传统重载的编译规定,可能会导致不可预期的问题。
  • ts 与 js 可交互性受影响,如果像传统重载那样,将函数拆分,在 js 脚本里调用 ts 中的重载办法将会有问题。

所以比起传统的重载,ts 的重载更像一个类型正文。

三、为什么要有 any

构想一个场景,一个函数须要承受一个数组,数组内数据能够是任意类型,到这如果想用泛型是没法完满解决的,所以还得引入 any,而 unkown 是起初引入来代替 any 的。但在个别的强类型语言通常不具备那么多灵活性,比方数组只容许一种类型,那就能够通过泛型来解决。

JSON.parse 的类型申明也是 any,因为当初还没有 unkown,不然应该返回 unkown 更正当。

四、图灵齐备的类型零碎

TypeScript 为了不削弱 JavaScript 的灵活性,同时又能提供足够的类型束缚,就带来了图灵齐备的类型零碎。

上面用类型来实现一个主动申明 N 个长度的数字元组,过程须要用到递归

type ToArr<N, Arr extends number[] = []>
    = Arr['length'] extends N // 判断数组长度是否达到
                ? Arr // 长度够则间接返回
                : ToArr<N, [...Arr, number]>; // 长度不够则递归

type Arr1 = ToArr<3>; // [number, number, number]

更进一步,甚至能够基于下面的 ToArr 再实现加法:

type Add<A extends number, B extends number> = [...ToArr<A>, ...ToArr<B>]['length'];
type Res = Add<3, 4>; // 7

甚至有人用 ts 的类型零碎实现了象棋规定。

五、readonly 和 as const

readonly 和 as const 都能将类型申明为 仅可读 ,而 as const 还能将类型 转换成常量。上面再看看两者的一些细节。

interface Foo {
    readonly a: {b: number,},
}

const f: Foo = {a: { b: 1},
};

f.a = {b: 2}; // Error: 无奈调配到 "a",因为它是只读属性

f.a.b = 2; // 这里则没问题

下面能够看出,readonly 只对以后对象无效,对其属性有效。但 readonly 对数组却能做到齐全不可批改。

const arr: readonly number[] = [2];
arr.push(1); // Error: 类型“readonly number[]”上不存在属性“push”

as const 将数组转化成元组,把一个可变长度的数组申明变成一个固定长度的数组:

const args = [8, 5]; // number[]
const func = (x: number, y: number) => {};
const angle = func(...args); // 这里会提醒谬误,因为 ts 不确定 args 是否有两个数

const args = [8, 5] as const; // 加上 as const,将 args 转换成 [number, number] 即可
args.push(2) // Error: 类型“readonly [8, 5]”上不存在属性“push”

六、类型束缚重置

在回调函数中已收窄的类型束缚将被重置,因为该回调可能会在异步代码后调用,外面通过闭包拜访的变量有被更改的危险,所以束缚重置是正当的。

具体看上面例子:

type MyType = {prop?: number | string,};
function func(arg: MyType) {if (typeof arg.prop === 'string') {
        const a = arg.prop; // string

        [].forEach(() => {
                        // 如果这里是异步回调,上面重写变量将会导致这里的 arg 扭转,所以束缚重置是正当的
            const b = arg.prop; // string | number | undefined
            console.log(b);
        });

                (() => {
                        // 立刻执行函数则不会重置类型束缚
            const d = arg.prop;  // string
            console.log(d);
        })();

                // 重写变量
                arg = {};}
}

这也是为了应答 js 的灵活性而须要的谨严。

七、协变和逆变

  • 协变:子类型兼容父类型,即 Array<Father>.push(Son),这是能够成立的,因为 Son 是 Father 的子类型,继承了所有 Father 的属性,所以对其兼容;
  • 逆变:父类型兼容子类型,与下面相同,具体看上面例子;
declare let animalFn: (x: Animal) => void;
function walkdog(fn: (x: Dog) => void) {}
walkdog(animalFn); // OK

这里 animalFn 的参数申明须要的是 Animal,但理论传入的是 Dog,下面的实质就是 (x: Dog) => void = (x: Animal) => void,所以参数是将 Animal 赋值给了 Dog,所以是 Animal 对 Dog 兼容,即逆变,如果将这个场景反过来,反而会出错。所以函数的参数是逆变,返回值是协变。

但 ts 的函数类型其实是双向协变的,但这并不平安,具体看上面例子:

declare let animalFn: (x: Animal) => void
declare let dogFn: (x: Dog) => void
animalFn = dogFn // OK,但这不平安
dogFn = animalFn  // OK

// 尽管在 ts 里像下面那样双向赋值(双向协变)是能够通过的,但这是不平安的

const animalSpeak = (fn: AnimalFn) => {fn(animal); 
};
animalSpeak((x: Dog) => {x. 汪汪() // 这里运行会报错,因为传入的 Animal,不具备 dog. 汪汪 办法
}); 

下面 animalSpeak 的调用,理论是将 Animal 作为参数赋值给了 Dog,这不满足函数参数逆变的准则,但在 ts 中却是能够通过编译的。

为什么 ts 容许函数双向协变:因为 ts 是结构化语言,如果 Array(Dog) 能够赋值给 Array(Animal),那么就意味着 Array(Dog).push 能够赋值给 Array(Animal).push,从而导致设计上就容许了双向协变,这是 ts 设计者为了维持结构化类型兼容的一种取舍。但毕竟双向协变是不平安的,所以在 2.6 版本后,开启严格模式,函数参数协变将会报错。对于双向协变具体能够看上面的例子:

interface Animal {eat: ''}
interface Dog extends Animal {wang: ''}

let animalArr: Animal[] = [];
let dogArr: Dog[] = [];
 
animalArr = dogArr; // OK

// Array<Animal>.push(Animal): number = Array<Dog>.push(Dog): number(参数协变)animalArr.push = dogArr.push; // OK

八、除了类型束缚,TypeScript 还带来了更残缺的面向对象

因为前端一部分的复杂度被 MVVM 框架消解了,并且过往 js 模仿的面向对象多少有点问题,所以我经常疏忽了面向对象的价值,然而 ts 带来了更加残缺、平安的封装、继承和多态。所以当前写 ts 的时候,多揭示本人编码能够多往这方面思考。

参考:
https://exploringjs.com/tackl…
https://www.zhihu.com/questio…
https://jkchao.github.io/type…
https://zhuanlan.zhihu.com/p/…

正文完
 0