TypeScript学习

  TypeScript,简称TS,就是给JS添加了类型系统,你可能会问,为什么要给JS添加类型系统呢?因为JS用法太过灵活了,3+[] 经过一系列的类型转化,竟然得到了字符串“3”, 如果程序出现问题,这种错误很难debug。加了类型,就能解决这类问题吗?是的,因为一旦定义了类型,你就知道,这个类型可以进行哪个操作,比如,number类型,可以进行算术运算,如果进行非法操作,就会报错。类型,就是定义取值的范围,以及对这些值的操作。学习TypeScript就是学习它的类型系统。学习之前,一定要先安装它, mkdir ts-learning && cd ts-learning && npm int -y 新建ts-learning项目, npm install typescript -D安装ts. touch type.ts,就可以写TypeScript代码了。

  TypeScript定义了各式各样的类型,如number, string, boolean等。那怎么给变量指定类型呢?格式为 变量名:变量类型。

let str: string;
let a: number = 3;

  如果在声明变量时,进行了初始化操作,像let a:number = 3; a是可以不用指定类型的,TypeScript 能够自动推断出a变量的类型是number类型。let a = 3; 当你把鼠标放到变量a上时,TypeScript 推断它是number类型。

TypeScript学习

  这也算是一种最佳实践吧,尽量让TypeScript 去推断类型,只有当TypeScript 不能正确地推断时,我们才指定类型。

  使用了类型系统后,程序要怎么运行呢?TS代码是不能直接运行的,要把它编译成JS代码,然后在浏览器或node 中执行。TypeScript 提供了tsc编译器,编译TS代码 到JS,当你npm install typescript 时,同时安装了tsc 编译器。但你要告诉它怎么编译,比如源文件在什么地方,编译到什么地方等等。这些信息放置的地方就是TS的配置文件tsconfig.json, 要把它放到项目的根目录下。执行tsc命令时,它会从项目根目录中寻找tsconfig.json, 然后根据配置信息进行编译。最简单的配置文件如下:

{
  "compilerOptions": {
    "rootDir": "./src",   /* .ts文件的根目录 */
    "outDir": "dist",     /* 编译后的文件放到哪里 */
    "target": "ES2015",   /* 编译到目标语法*/
    "module": "commonjs"   /* 编译后文件使用哪一种规范*/
  }
}

  npx tsc 进行编译,项目就多出来了dist目录。node dist/type.js就可以运行程序了。假设粗心大意,给str赋值了数字3,

let str: string;
str = 3;

  你会发现str变量下面标红了,鼠标移动上去,看到了报错的原因,数字类型不能赋值给字符串类型。怎么这么神奇呢?npm install typescript,除了安装了刚才说的tsc编译器,还安装了tsserver(TypeScript Language Service), 编辑器或IDE利用tsserver来完成标红提示错误等功能。当你编写TS代码时,编辑器会时时地与tsserver 进行通信,实实编译代码(编译到内存中)。这里要注意的是VS Code使用的TS版本,它默认使用的是Node自带的tsc编译器,当你打开任意一个.ts文件时,在VS Code的右下方,你都能看到这个版本号,

TypeScript学习

  如果想使用自己安装的TS版本,就点击这个版本号,VS Code会在编辑区的上方弹出一个选择框,点击选择想要的版本就可以了。说的有点远了,现在再执行npx tsc会发生什么?同样报错,tsc 在编译的过程中,它会类型检查(type check),但你也发现, tsc 仍然把错误的TS代码编译成了JS代码。按理说,如果有错,就不应该编译,更不应该输出编译后的JS文件。 幸好,可以在tsconfig.json配置noEmitOnError: true, 如查TS检查出错误,它就不会编译JS代码。

  无论是VS Code编辑器的实时编译,还是tsc 的编译,你都会发现TS给JS加入类型系统的一个好处,就是尽可能早的发现问题,就是尽量把运行时发生的错误在编译时捕获,不用运行代码,也能检测到在程序运行过程中引起问题的代码。

  number, string, boolean都是JS中拥有的类型,行为也类似,就不说了,主要说TypeScript中定义的JS中没有的类型。

  any: 任意类型

  当一个变量是any类型时,它可以被赋值给任何值。

let str: any;
str = 2;
str = 'str';

  同时,any类型的值也可以赋值给任意其它类型。

let a: any = 'string';
let b: number = a;

  当使用any类型时,TS不会进行类型检查,代码很容易出错,所以尽量不要用any 类型。

  unknown:不知道的类型

  为什么会有这种类型呢?在极少的情况下,你事先并不知道一个变量要保存什么类型,需要一个类型标注。你可能会说不是有any吗?确实,可以声明为any类型,但一旦声明为any类型,这个变量就不受控制了,就可以对这个变量进行任何操作,但你肯定不想对一个变量进行任意的操作,报错了怎么办?unknown类型和any类型不同之处就是在于控制操作上。当一个变量是unkonwn类型时,你做的事情并不多,如果要使用unknown类型,在使用之前,一定缩小它的类型范围(narrow type)。举个例子,就明白了

let a: any;
a = 3;
console.log(a.toUpperCase());

let b: unknown;
b = 3;
// Property 'toUpperCase' does not exist on type 'unknown'. console.log(b.toUpperCase());

  假设事先并不知道a和b的类型,于是分别把a和b标注了any 和unkown类型。先看a(any类型),你可以把任何值赋给它,于是赋值了3,你可以对其进行任何操作,于是调用方法toUpperCase(),实际上数字3并没有toUpperCase()方法,尝试去调用对象不存在的方法,就会导致程序运行时报错。

  再看b,编译器直接报错了,toUpperCase属性不存在unknown类型上。这就是区别。当你声明一个unknown类型的变量时,在对其进行操作(调用方法)之前,编译器会强制你进行向下类型缩小,比如你要对这个类型进行判断,有没有这个属性,这就减小了潜在的运行时报错。缩小类型范围的方法有很多,这里使用typeof, 我们都知道只有字符串才调用toUpperCase()方法,那就判断b是不是字符串

let b: unknown;
b = 3;
if (typeof b === 'string'){
    console.log(b.toUpperCase());
}

  unknown和any一样,可以表示任意的值 ,但后续的操作却完全不同。any可以进行任意的操作,但unknown却要进行类型缩小,也因此更为安全。如果实在是不知道变量的类型,尽量使用unknown,不要使用any。

  null和undefined 类型

  它们也是JS中已有的类型,之所以把它们拿出来说,是因为,在TypeScript中,null和undefined可以赋值给其它任意类型,而不仅仅是null和undefined类型。

const x: number = null; 

  竟然没有报错,x是number类型,值却是null,如果用x调用number的方法,肯定报错,最好是null只能赋值给null类型,undefined只能赋给undefined类型。这要配置strictNullChecks来控制null或undefined是否能赋值给其它类型。strictNullChecks: true 表示禁止赋值给其它类型。当在tsconfig.json中写入strictNullChecks: true

// Type 'null' is not assignable to type 'number'
const x: number = null; 

  如果变量值就是null,那要明显地标注变量是null类型

const x: null = null; 

  字面量类型

  TypeScript 还定义了字面量类型,3也是一个类型。变量类型的声明方式是 变量名:类型,如果类型是一个字面量时,就是字面量类型。

let a: 3;
let b: 'name';
let c: false;

  当变量类型是字面量类型时,它的取值也就固定了。a只能赋值为3,b只能赋值为'name',c只能赋值为false。

  数组类型

  在使用数组时,数组中的元素通常是一个类型,所以数组的类型声明是元素的类型后面跟上[]

let arr: number[]; // arr数组中每一个元素都是number类型

  数组还有一个子类叫元组类型,看一下类型声明就清楚了

let turple: [number, string];

  [number, string] 就是一个元组类型,[ ]表示它是一个数组类型,但元素的类型声明不是在[ ] 的外面,而是在里面。这表明,数组中元素都是固定的,也就是数组的长度是固定的,并且数组中的每一个元素的类型也是固定的,所以元组就是一个元素类型固定且长度固定的数组。turple 就是2个元素的数组,数组的第一个元素的类型是number,第二个元素的类型是string。

let arr: number[] = [2, 3, 4, 5];
let turple: [number, string] = [2, 'name'];

  对象类型

  在TS中有4种方式来标示一个变量(或值)是对象类型

let obj1: object = {a: 2}; // 小写object 类型
let a: {b: number} = { b: 12 }  //对象字面量语法,{b: number}类型
let danger: {} // {} 类型
let obj: Object; // 大写的object类型。

  先看第一种,小写的object类型。

let obj1: object = {a: 2};
// Property 'a' does not exist on type 'object'
obj1.a;

  获取obj1对象上a属性,发现报错了,object类型上不存在属性a。当声明一个变量是object类型时,只表示它是一个object,而不是null,再也没有其它过多信息了。你对object类型(obj1)什么也做不了。

  再看{} 类型和Object类型。

let danger: {};
danger = {};
danger = {x: 1}
danger = [];
danger = 2;

let Obj: Object;
Obj =1;
Obj = {x: 1};

  除了null和undefined以外的类型都能赋值{} 类型和Object类型,这两种声明方式没有什么用。

  以上三种声明对象类型的方式,几乎没有什么用,就只剩下对象字面量的声明方式。对象字面量语法声明对象类型,声明的是对象的形状,对象包含哪些属性。这样声明对象类型的方式,叫作structurally typed(duck typing )。structurally typed(duck typing )是一种编程风格,它只关心对象有哪些属性,而不关心它名义上的类型。

  当把一个对象字面量赋值给一个对象类型变量时,TS会执行非常严格的类型检查。对象字面量中的属性,既不能比对象类型中的定义的属性多,也不能比类型中的定义的属性少,必须一一对应,否则报错,这叫excess property check

let obj: {b: number} = {
    b: 123,
    /* 
    Type '{ b: number; a: number; }' is not assignable to type '{ b: number; }'.
    Object literal may only specify known properties, and 'a' does not exist in type '{ b: number; }'
    */
    a: 456
}

  但这会带来问题,有的对象有某个属性,有的对象却没有这个属性,有的对象有很多属性,有的对象却没有这些属性,但它们都有共有的属性a,这样怎么声明,可使用?: 标示某个属性可有可无,也可以使用索引表达式 [key: number]: boolean。 key(可以取任意名字)表示对象的属性,是number类型,值是布尔类型。

let obj: {
    b: number
    c?: string
    [key: number]: boolean
}

   obj 必须有b属性,且是number类型。c属性可能有也可能没有,如果有c属性,它的值可以是undefined。如果还有其属性,它们必须是number类型,且属性值必须是布尔值。

obj = {b: 3};
obj = {b: 2, c:undefined};
obj = {b: 2, 10: false}
obj = {10: false} // 报错,没有b属性。

  函数类型

  无论是函数声明,还是函数表达式,在写函数的过程中可以直接定义其参数类型和返回值。

function sum(a: number, b: number): number {
    return a + b;
}
const substract = (a:number, b:number):number => a - b;

  参数a,b都是number类型,参数列表() 后面跟 :number,表示函数的返回值类型,是number类型。如果TS能推断出函数的返回值类型,返回值类型可以省略。

function sum(a: number, b: number) {
    return a + b;
}

  当你把鼠标放到sum上时,可以看到TS已经推断出函数的返回值是number类型。如果参数类型也不写呢?

function sum(a, b) {
    return a + b;
}

  鼠标放到a上,a是any类型。当TypeScript不能推断一个变量的类型时,它就默认变量的类型为any。这叫隐式any,因为你没有写any这个关键字,变是却是any类型。由于any类型,容易引起问题,所以最好不要让TS做这种类型推断。如果参数确实是any类型,要显示地标示出来。配置项noImplicitAny 就是阻止TS做这种类型推断的。当在tsconfig.json文件中配置noImplicitAny: true时,如果你没有为变量标注类型,而是让TS推断出了它的类型是any时,TS就会报错。

function sum(a, b) {// Parameter 'a' implicitly has an 'any' type.
    return a + b;
}

  如果函数没有返回值呢?返回值类型是void

function print(a: string): void {
    console.log(a);
}

  当函数抛出错误呢,返回值是never类型,永远不会发生。never类型是所有类型的子类型,你可以把never类型赋值给任意其它类型。

function erorr(): never {
    throw new Error('Error');
}

  有时函数参数是默认参数或可能传参,也可能不传参的参数,参数类型用?: 表示。

function request(url: string, method?: string){}

  函数中还有一个特殊的存在this,函数调用方式不同,this指向就不同,这也是问题的所在,所以函数中有this时,最好声明this的期望类型。在TypeScript中,可以在参数列表中声明this,不过,this要作为第一个参数存在,其它参数放到它后面

function fancyDate(this: Date, name?: string) {
    return `${this.getDate()}/${this.getMonth()}/${this.getFullYear()}`
}

fancyDate.call(new Date)

  当函数作为参数传递,或者作为函数返回值时,就不能用这种方式声明类型了,那就需要自定义类型了。

  使用type自定义类型

  使用type自定义类型,格式为:  type 类型名称 = 类型。 以sum函数为例,它的类型是什么呢?两个number类型参数,返回一个number类型,类型就可以这么写

(a:number, b:number ) => number

  很像箭头函数的语法,参数列表 => 返回值类型。再取个名字Sum,自定义类型就是

type Sum = (a:number, b:number ) => number;

   Sum就是一个自定义的函数类型,只要一个函数接受两个number类型的参数,并返回number类型,这个函数就可以用Sum 标示。

let sub: Sum = (a, b) => a - b;

  同时可标注参数或返回值

function runSum(fn: Sum, a:number, b: number) {
    return fn(a, b);
}
function returnSum(): Sum {
    return  (a, b) => a + b ;
}

returnSum()(7, 8);

  type Sum = (a:number, b:number ) => number; 是Sum类型的简写形式,更完整的形式如下

type Sum = {
    (a: number, b: number): number
}

  因为在JS中函数是一个对象,对象就会有属性,声明一个函数类型,也就是声明一个对象类型,就可以用对象字面量的方式声明它。

  使用Type 来定义类型的时候,有两个重要概念,type union(联合类型),type insertection(交叉类型)。

  type union(联合类型)

  type union(联合类型):两个或多个类型使用 | 联合在一起,形成一个新的类型。可以把两个基本类型联合起来

type StrOrNum =  string | number;

   StrOrNum 或是string类型,或是number类型。使用这个联合类型时,只能使用string和number类型的共用的属性,两者很少共有属性。可以把两个对象类型联合起来

type Cat = {name: string, purrs: boolean}
type Dog = {name: string, barks: boolean, wags: boolean}
type CatOrDogOrBoth = Cat | Dog

  当你看到一个类型是CatOrDogOrBoth的时候, 你想到了什么?保险起见,我只使用它们共有的属性name,这个类型肯定有一个name属性,其它属性有没有,不敢保证。反过来想,既然这个类型是CatOrDogOrBoth,我可以给它赋一个cat值,也可以给它赋一个Dog值,共有的值也可以赋给它。

let c:CatOrDogOrBoth = {name: 'cat', purrs: false } // Cat类型
let d:CatOrDogOrBoth = {name: 'dog', barks: false, wags: false } // dog类型
let cdBoth: CatOrDogOrBoth = { name: 'both', barks: true, purrs: true, wags: true}

  类型联合时要注意,如果两个类型中有相同的属性,但属性却是不同的类型时,属性的两个类型也进行联合(union)

type Product = {
    id: number,
    name: string,
    price?: number
};
type Person = {
    id: string,
    name: string,
    city: string
};

// Product 和 Person 联合之后的结果
type UnionType = {
    id: number | string,
    name: string
};

  UnionType就是Product和Person类型进行联合后生成的新类型。在UnionType中,id属性的类型是联合类型 number | string ,就是因为id 在Product中是number 类型,但是在Person类型中是string类型.  name属性在两个类型中都是string, 所以在新的联合类型中,name属性也是string

  type insertection(交叉类型)

  type insertection(交叉类型):两个或多个类型使用 & 联合在一起,形成一个新的类型。

type strAndnum =  string & number;

   strAndnum,既是string,又是number,新的类型集合了string类型 和number类型的所有属性。正是由于把所有的属性都集合到了一起,你会发现,没有一个值既是string,又是number,所以这个类型没有意义。通常,&用于对象类型。

type Person = {
    id: string;
    name: string;
    city: string;
}

type Employee = {
    company: string;
    dept: string
}

type PersonAndEmpoyee = {
    id: string;
    name: string;
    city: string;
    company: string;
    dept: string
}

  PersonAndEmpoyee就是Person和Empoyee进行交叉生成的新的类型。你可以看到,type insertection(交叉类型) 就是把所有对象类型的属性全部组合到一起,允许你使用所有的属性,而不是共有类型。

TypeScript学习

   当给一个交叉类型进行赋值时,值必须包含每一个类型中都定义的属性

let bob: Person & Employee = {
    id: "bsmith", name: "Bob", city: "London",
    company: "Acme Co", dept: "Sales"
};

  如果两个要交叉类型有相同的属性时,这时要注意。如果相同的属性,类型也相同,这没有问题。给Empoyee再声明一个id:string属性,Person & Employee 的结果还是一样  

 TypeScript学习

 

   但是如果类型不同,这个属性的类型,是两个不同类型进行交叉,分别给Person和Employee 定义一个contact属性,类型不同

type Person = {
    id: string;
    contact: number;
}

type Employee = {
    id: string;
    contact: string;
}

  Person & Employee

type PersonAndEmpoyee = {
    id: string;
    contact: number & string;
}

TypeScript学习

   这时就出现问题了,没有一个值,它既是number 又是string. 解决办法,就是相同的属性不要使用原始类型,要使用对象。  

type Person = {
    id: string;
    contact: {phone: number};
}

type Employee = {
    id: string;
    contact: {name: string}
}

type PersonAndEmpoyee = {
    id: string;
    contact: {phone: number} & {name: string};
}

TypeScript学习

 

    

 

  

 

上一篇:Typescript爬取网站信息


下一篇:React-typescript中ref定义