文章目录
零.介绍
1.TypeScript是什么?
微软开发的开源编程语言,是JS的超集,可以视为加了Type(类型)的JavaScript
let age: number = 18
2.为什么要添加类型支持?
原因一
JS代码中绝大部分错误都是类型错误(Uncaught TypeError
),影响效率,这是主要原因
原因二
JavaScript需要等代码真正去执行时才能发现错误,
TypeScript在代码编译时就能发现,若再结合vscode等开发工具,能提前到编写代码的同时就能发现错误,
减少找bug和改bug的时间
3.TS的优势
- 更早发现错误
- 任何位置都有代码提示
- 类型系统提升了代码的可维护性
- 支持最新的ECMAScript语法
- TS的类型推断机制使得不需要在代码的每个地方都标注类型
一.安装和使用
1.安装编译TypeScript的工具包
由于nodejs或者浏览器只认识js代码,需要将ts代码转换为js代码才能运行
命令:npm i -g typescript
typescript包:用来编译ts的包,提供了tsc命令,可以生成同名js文件
验证安装成功:tsc -v
2.编译并运行TypeScript代码
- 新建
hello.ts
文件
console.log('hello typescript')
- 生成同名js文件:
tsc hello.ts
- 执行js文件:
node hello.js
3.简化上述步骤
把生成同名js文件那一步省略,直接执行ts文件
- 安装ts-node包:
npm i -g ts-node
- 执行ts文件:
ts-node hello.ts
踩坑:遇到了版本冲突的报错,先统一一下使用版本
npm install -D ts-node@10.9.1 typescript@5.0.4 @types/node@18
如果vscode提示报错,就在js文件的最后加上:
export {}
二.TypeScript的常用类型
js有数据类型,但不会检查变量的类型是否发生了变化,
ts可以显示标记出代码中的意外行为
let count: number=100
count='200'//ts会提示
1.类型注解
上例中的number就是类型注解,它为变量添加类型约束,指定count的类型为数值类型
2.TypeScript的基础常用类型分为JS已有类型和TS新增类型
js已有类型
- 原始类型:
number,string,boolean,undefined,null,symbol
- 对象类型:
object(包括对象,数组,函数)
ts新增类型
联合类型,自定义类型(也叫类型别名),接口,元祖,字面量类型,枚举,void,any
等
3.原始类型
let age: number = 18
4.对象类型(数组/函数/对象)&联合类型&void类型&函数可选参数
4.1.数组(两种写法)
let arr: number[] = [1,3,5](推荐)
let arr: Array<number>=[1,3,5]
//如何实现数组中既有数字又有字母?==>联合类型,格式:(number|string)[]
let arr2= (number|string)[]=[1,'a',2,'b']
4.2.函数(两种写法)
/*1.单独指定参数和返回值的类型*/
//函数作为类型声明
function add1(a: number, b: number): number {
return a + b;
}
//函数作为表达式
const add2 = (a: number, b: number) => {
return a + b;
}
/*2.同时指定参数和返回值的类型*/
//当函数作为表达式时,可以通过类似箭头函数的语法来为函数添加类型
const add: (a: number, b: number) => number = (a, b) => {
return a + b;
}
- 若函数没有返回值,那么函数返回值的类型为
void
function noReturn(name:string): void {
console.log('no return', name);
}
noReturn('张三');
- 用
"?"
标识符定义函数可选参数
,可传也可不传
function add3(a: number, b?: number): number {
return a + (b || 0);
}
console.log(add(2))
//注:可选参数只能出现在参数列表的最后,其后不能再出现任何必选参数
4.3. 对象
- 对象有属性与方法,ts中对象的类型就是用来描述对象结构的
let person: { name: string; age: number;sayHello():void } = {
name: '张三',
age: 30,
sayHello() {
console.log('hello', this.name);
}
};
- 说明:
1.直接用{}
来描述对象结构,属性采用属性名:类型
的形式,方法采用方法名():返回值类型
的形式
{ name: string; age: number;sayHello():void }
2.若方法中有参数,就在方法名后()中指定参数类型
sayHello(name:string)
3.在一行代码中指定对象的多个属性类型时用分号来分隔
略
4.方法的类型也可以使用箭头函数的形式
- 对象的属性和方法也能设为可选参数
function myAxios(config: { url: string; method?: 'GET' | 'POST'; data?: any }) {
console.log('myAxios', config);
}
myAxios({ url: 'http://www.baidu.com' });
5.type关键字/自定义类型/类型别名
即为任意类型取别名
使用场景:当同一(复杂)类型被多次使用时,可以通过类型别名简化使用过程
上例中的联合类型(既有数字又有字母的类型)定义为customArray类型
type customArray=(number|string)//type关键字:创建类型别名
let arr2: customArray[]=[1,'a',2,'b']
6.接口(interface)
6.1.当一个对象类型被多次使用时,可用接口来描述对象的类型以复用
使用interface
关键字声明接口,直接把接口名称作为变量类型
interface Person {//接口名首字母大写
name: string;
age?: number;
sayHello?(): void;
}
let p: Person = { name: '李四' , sayHello() { console.log('hello', this.name); } };
console.log(p.name);
//调用方法时,要先判断是否存在
if (p.sayHello) {
p.sayHello();
}
6.2.接口interface与类型别名type的对比
相同:都能给对象指定类型
不同:
- 接口:只能为对象指定类型
- 类型别名:可以为任意类型指定别名
//interface
interface Person1 {
name: string;
age?: number;
sayHello?(): void;
}
//type
type Person2 = {
name: string;
age?: number;
sayHello?(): void;
}
//分别创建两个对象,一个使用interface,另一个使用type定义的对象
let person1: Person1 = { name: '王五' };
let person2: Person2 = { name: '赵六' };
console.log(person1, person2);
// 再举一个type为非对象指定类型的例子
type Name = string;
let name: Name = '张三';
console.log(name);
6.3.接口继承
若两个接口之间都有相同的属性或方法,可以将公共的部分抽离出来,通过继承实现复用
interface Point2D {
x: number;
y: number;
}
interface Point3D extends Point2D {
z: number;
}
/*
1.使用extends关键字实现继承
2.继承后就有了被继承对象的所有属性和方法
*/
7.元祖类型(Tuple)
- 使用场景
坐标数据
是有且仅有两个数字构成的数组,但若使用数组,后续可以添加任意多个数字,这样不严谨
- 解决方法
使用元祖类型Tuple,它是另一种类型的数组,确定包含多少个元素和特定索引对应的类型
//使用数组类型来记录经纬坐标的例子:
let position1: number[] = [1, 2];
//使用元祖类型来记录经纬坐标的例子:
let position2: [number, number] = [1, 2];
元祖类型可以确切标记出有多少个元素,和每个元素的类型
8.类型推论
ts中某些没有明确指出类型的地方,ts的类型推论机制会帮助提供类型
比如:1.声明变量并初始化时;2.决定函数返回值时,可以不写类型注解
let age:number=18//不写number
function add (a:number,b:number):number{//不写返回值的类型mumber
return a+b
}
9.类型断言
开发人员比编程器更明确一个值的类型时,应该使用类型断言指定更具体的类型,使其跳过ts的类型检查
常见使用场景:
getElementById方法返回值的类型是HTMLElement,该类型太宽泛不够具体,包含所有标签公共的属性和方法,而我们的需求是获取a标签的href属性
解决方法:是使用类型断言执行更加具体的类型
<a id="link" href="https://tslang.org">TypeScript官网</a>
<!-- 引入编译后的JS文件 -->
<script src="link.js" defer></script>
//link.ts
const linkElement = document.getElementById('link') as HTMLAnchorElement;
console.log('获取的链接地址:', linkElement.href);//获取的链接地址: https://tslang.org/
生成同名js文件:tsc link.ts
10.字面量类型
字面量类型用来表示一组明确的可选值列表,常和联合类型配合使用,相比于string类型,使用字面量类型更精确和严谨
示例:在控制游戏角色的函数中,方向的可选值只能是上下左右
// 字面量类型的例子:控制游戏角色的移动方向
type Direction = 'up' | 'down' | 'left' | 'right';
function move(direction: Direction) {
console.log('moving', direction);//参数direction的值只能是上下左右中的一个
}
move('up');
move('down');
move('left');
move('right');
11.枚举类型(enum)
11.1.什么是枚举类型?
枚举:定义一组命名变量,用来描述一个值,这个值可以是命名常量中的一个
枚举的功能类似于字面量类型+联合类型组合,它也能用来表示一组明确的可选值
//枚举类型的例子:控制游戏角色的移动方向
enum Direction2 {
Up = 'up',
Down = 'down',
Left = 'left',
Right = 'right'
}
function move2(direction: Direction2) {
console.log('moving', direction);
}
注意事项:
1.使用enum关键字定义枚举
2.约定枚举名称:枚举中的值以大写字母开头Up,Down...
3.枚举中的多个值之间通过逗号分离
4.直接使用枚举名称Direction2作为类型注解
11.2.枚举成员的值:数字枚举和字符串枚举
我们把枚举成员作为了函数的实参,它的值是什么?
上例中,若不设置值.或者只设置Left=10
,那么它们的默认值
分别为0,1,2,3或10,11,12,13,此时它们称为数字枚举
,
上例中设置"left,right,top,bottom"后称为字符串枚举
11.3.枚举的特点和原理
枚举是TS的非JS类型级扩展的特性之一
其他类型仅仅被当做类型,在编译为js代码时会被自动移除,而枚举还提供值(枚举成员都是有值的),所以也会编译成js代码
//生成的hello.js中枚举代码仍存在
var Direction2;//在枚举类型定义之前,先声明一个空对象,以便后续为其添加属性
(function (Direction2) {
Direction2[Direction2["Up"]] = "Up";//为空对象Direaction2添加属性,并赋值
Direction2[Direction2["Down"]] = "Down";
Direction2[Direction2["Left"]] = "Left";
Direction2[Direction2["Right"]] = "Right";
})(Direction2 || (Direction2 = {}));//自执行函数,调用的同时定义了枚举类型
function move2(direction) {
console.log('moving', direction);
}
枚举
VS字面量类型+联合类型
,都能表示一组明确的可选值列表,
选后者:更直观简洁和高效
12.any类型
能不用就不用any类型.这会使typescript变成anyscript,失去ts的类型保护的优势
语法:添加any关键字即可
//any类型的例子:
let anyType: any = 'hello';
anyType = 123;
anyType = true;
anyType = { name: '张三' };
console.log(anyType);
使用场景:临时使用any来避免书写很长很复杂的类型
13.typeof操作符
JS中的typeof:获取数据类型
TS中的typeof:类型查询,在类型上下文中引用变量或属性的类型
使用场景:根据已有变量的值获取该值的类型,简化类型书写
示例一:
let str = 'hello';
type StrType = typeof str;//StrType等价于string类型
function getStr(): StrType {//实际上就是返回string类型
return 'world';
}
console.log(getStr());
示例二:
let obj = { name: '李四', age: 30 };
type ObjType = typeof obj;
function printObj(obj: ObjType) {//function printObj(obj: typeof obj)
console.log("输出:",obj);
}
printObj(obj);//若printObj(1),会报错
1.使用typeof操作符来获取变量obj的类型是Object
2.typeof出现在类型注解的位置(obj:typeof obj),所处的环境就是在类型上下文
3.注意:typeof只能用来查询变量或属性的类型,无法查询其他形式的类型(如:函数调用的类型)
三.TypeScript高级类型
高级类型:class类,类型兼容性,交叉类型,泛型和keyof,索引签名类型和索引查询类型,映射类型
1.class类
TS支持ES6新增的class关键字,并为其添加了类型注解和其他语法(eg:可见性修饰符等)
class Person {
name: string;//写法一:在类中定义属性并后面加上类型注解,在构造函数中初始化
id='1001';//写法二:声明并设置默认值,让ts推断出属性的类型。
constructor(name: string) {//构造函数
this.name = name;
}
}
const person = new Person('张三');
console.log(person);
根据TS的类型推论,p的类型是Person
const p:Person
说明TS中的class也作为一种类型存在
1.1.构造函数
解释:
1.成员初始化后才能通过this.属性来访问实例成员
2.需要为构造函数指定类型注解,否则隐式推断为any
3.构造函数不需要返回值类型
1.2.类的实例方法
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
/* sayHello() {
console.log(`hello,${this.name}`);
}*/
sayHello(str:string): void {//class方法的参数和返回值与函数类似
console.log(str,`,我叫${this.name}`);
}
}
const p = new Person('张三');
p.sayHello("你好");
方法的类型注解和函数用法相同
1.3. 类的继承
1.3.1.extends继承父类
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
//继承Person类,并扩展了新的属性
class Student extends Person {//通过extends实现继承
id: number;
constructor(name: string, age: number, id: number) {
super(name, age);//调用父类的构造器初始化父类属性
this.id = id;//初始化子类属性
}
}
const stu=new Student("张三", 18, 10023456789);
console.log(stu);
1.3.2.implement实现接口
//定义一个接口:接口是对象类型的规范,规定了对象的结构,但不提供具体实现
interface Person {
name: string;
age: number;
}
//Student类中必须提供Person接口中指定的所有属性和方法
class Student implements Person {//使用implements关键字,实现接口继承Person类
constructor(public name: string, public age: number) { }//实现接口的类必须实现接口的所有属性
}
const stu = new Student('张三', 18);
console.log(stu);
1.4.类成员的可见性修饰符:public,protect,private
//public:公有成员,可以被任意对象访问
//protected:受保护成员,只能被其所属类及子类访问
//private:私有成员,只能被其所属类访问
class Person {
public name: string;
protected id: number;
private age: number;
constructor() {
this.name = "张三";
this.id = 100;
this.age = 20; //私有成员:只能在构造函数中被访问
}
}
//子类不能访问父类的私有成员,但是可以访问父类的公有和受保护成员
class Student extends Person {
constructor() {
super(); //调用父类的构造函数
console.log(this.id); //受保护成员:可以访问
}
}
const stu = new Student();
//class外访问受保护成员和私有成员会报错,但公有成员可以在类外访问
console.log(stu.name); //公有成员:可以在类外访问
//受保护成员对实例不可见,但可以在子类的方法内部来this访问
1.5.类成员的只读性修饰符:readonly
添加了readonly的属性只能在构造函数内进行赋值
class Person {
readonly name: string;
constructor(name: string) {
this.name = name;
}
}
//验证:readonly修饰符保证了构造函数赋值之后,name属性不可以再次被赋值
let person = new Person('张三');
console.log(person);
// person.name = '李四'; //错误,不能赋值
- 只读修饰符readonly,只能修饰属性不能修饰方法
- name后面的string若不加,会变成字面量类型
- 接口或对象{}也可以使用readonly
2.类型兼容性
类型兼容性不是特指某种TS高级类型,而是TS类型系统的特性
2.0.类型系统有哪些?
- 结构化类型系统(Structual Type System)
- 标明类型系统(Nominal Type System)
TS采用的是结构化类型系统,也叫作鸭子类型(duck typing),类型检查关注的是值所具有的形状
即:在结构类型系统中,若两个对象具有相同的形状,则视为同一类型
(鸭子类型:若它听起来看起来叫起来都像一只鸭子,那它就是一只鸭子)
示例一:结构化类型系统中,具有相同形状的两个类被视为同一类型
class Point{
x:number;
y:number;
}
class Point2d{
x:number;
y:number;
}
let p:Point = new Point2d(); //正确,因为两者形状相同
/*
p的类型被显式标注为Point类型,但它的值是Point2D的实例,在结构化类型系统(ts)中,这不会发生类型错误,
但在标明类型系统(c#,java)中,它们是不同的类,类型无法兼容
*/
示例二:js数组的forEach方法中使用了类型兼容性
//在这个回调函数中已经发生了类型兼容性,
let arr:string[] = ['a','b','c'];
arr.forEach((item)=>{//回调函数的类型:callbackFn: (value: string, index: number, array: string[]) => void
console.log(item); //item的类型是string类型
})
/*在TypeScript中,数组的forEach方法有一个特殊的类型推断规则
在数组的forEach方法中,回调函数的参数类型是根据数组的类型推断出来的*/
//由于有类型兼容性的存在,对数组的遍历可以传入不同数量的参数,如:
arr.forEach((item)=>{
console.log(item);
})
arr.forEach((item,index)=>{
console.log(item,index);
})
arr.forEach((item,index,array)=>{
console.log(item,index,array);
})
2.1.类之间的类型兼容性
前面提到:在结构化类型系统中,若两个对象具有相同的形状,则认为它们属于同一类型
这并不够精确,应为:对于对象类型来说,y的成员至少与x相同,那么称x兼容y
(成员多的可赋值给成员少的)
class Point {
x: number;
y: number;
}
class Point3D{
x: number;
y: number;
z: number;
}
const point: Point = new Point3D()
//Point3D的成员至少与Point相同,此时称:Point兼容Point3D
//即:成员多的Point3D赋值给成员少的Point没有问题,反过来则不行
2.2.接口之间的类型兼容性
- 接口之间的兼容性:如果一个类型兼容另一个,那么前者可以被赋值给后者
interface Point {
x: number;
y: number;
}
interface Point2D {
x: number;
y: number;
}
interface Point3D{
x: number;
y: number;
z: number;
}
let p1: Point = {x:1,y:2}
let p2: Point2D
let p3: Point3D={x:1,y:2,z:3}
//接口之间的兼容性:如果一个类型兼容另一个,那么前者可以被赋值给后者
p2=p1//可以兼容
p1=p3//可以兼容
//p3=p1//报错,因为p3是多出来的属性z,所以不兼容
- class和interface之间也能兼容
interface Point {
x: number;
y: number;
}
interface Point2D {
x: number;
y: number;
}
class Point3D{
x: number;
y: number;
z: number;
}
let p3:Point2D=new Point3D()
2.3.函数之间的类型兼容性
考虑到参数个数,参数类型和返回值类型
,函数之间的兼容性比较复杂,分类讨论:
2.3.1.参与个数
省略用不到的函数参数是常见操作(如遍历数组时forEach的回调),
js中这种普遍的使用方式,促成了ts中函数类型之间的兼容性
//示例:参数多的兼容参数少的(少的能赋值给多的)
type F1 = (a: number, b: string) => void;
type F2 = (a: number) => void;
let f1: F1 = function(a, b) { };
let f2: F2 = function(a) { };
f1 = f2; // f2参数少,能给参数多的f1赋值,此时称为f1兼容f2
//f2=f1//反过来不行
2.3.2.参数类型
对于原始类型
,要求相同位置的参数类型要相同
对于对象类型
,要求相同位置的参数类型要兼容
- 原始类型
//参数是原始类型时,参数类型要相同
type F1=(a:number)=>void;
type F2=(a:number)=>void;
let f1:F1;
let f2:F2=f1
f1=f2;//也ok
//函数类型F1兼容F2,因为F!和F2的第一个参数类型相同
- 对象类型
//参数是对象类型
interface Point2D{
x:number;
y:number;
}
interface Point3D {
x:number;
y:number;
z:number;
}
type F3=(a:Point2D)=>void;
type F4=(a:Point3D)=>void;
let f3:F3;
let f4:F4=f3;
f3=f4; //此处报错,在接口兼容性的逻辑是成员多的F4赋给成员少的F3是可以的
//这与与前面提到的接口兼容性冲突,如何更好地理解?
//答:把对象拆开,把每个属性看做一个个参数,变成参数少的可赋值给参数多的情况
2.3.3.返回值类型
返回值类型的规则只需要关注返回值本身就可以了
- 原始类型
//返回值类型是原始类型:两个类型要相同
type F5=()=>string
type F6=()=>string
let f5:F5=()=>'hello'
let f6:F6=f5
- 对象类型
//返回值类型是对象类型:成员多的可以赋值给成员少的
type F7=()=>{a:number}
type F8=()=>{a:number,b:string}
let f7:F7=()=>{return {a:1}}
let f8:F8=f7 //报错,因为F8的返回值的属性比F7的多一个属性b
3.交叉类型(&)
类似接口继承extends
关键字.用于把多个类型组合成一个类型(常用于对象类型)
interface Person {
name: string;
}
interface Person2 {
age: number;
}
type Person3 = Person & Person2; // 交叉类型,相当于合并了两个接口的类型
let person1: Person3 = { name: 'Tom', age: 20 };
console.log(person1);
交叉类型&和接口继承extends的区别:
//交叉类型&和接口继承extends的区别
//相同点:都可以实现对象类型的组合
//不同点:实现类型组合时,对于同名属性之间,处理类型冲突的方式不同
interface A{
fn:(value:number)=>string
}
interface B extends A{
fn:(value:string)=>string
}
//接口继承会报错因为类型不兼容
interface C{
fn:(value:number)=>string
}
interface D{
fn:(value:string)=>string
}
type E=C&D
//交叉类型不会报错因为同名属性会合并
// 相当于
interface E{
fn:(value:string | number)=>string;
}
4.泛型
泛型是TS中功能强大的高级类型,它能在保证安全的前提下让函数和多种类型一起工作,从而实现复用
使用场景:函数,接口,class
4.1.场景需求
function id(value: number): number { return value}
id(10)//返回10此时只接收数值类型,无法用于其他类型
//改成any
function id(value: any): any { return value}
//这样会失去ts的类型保护,类型不安全
泛型,可以既能保证类型安全(不丢失类型信息)的同时,又能让函数(接口,class)与多种类型一起工作,可实现灵活复用
实际上在C#和java等编程语言中,泛型都是用来实现可复用组件功能的主要工具之一
4.2.泛型的使用:实现场景需求
//创建泛型函数
function id<Type>(value:Type):Type{return value}
//调用泛型函数
function fn<Type>(val:Type):Type{return val}
console.log(fn<number>(123),fn<object>({name:"张三"}))
解释:
1.语法:在函数名称后添加尖括号,里面放类型变量Type
2.类型变量Type是一种特殊类型的变量,它处理类型不是值
3.该类型变量相当于一个容器,捕获用户提供的类型(用户在调用该函数时指定)
4.由于Type是类型,因此可以将其作为函数参数和返回值类型,表示参数和返回值具有相同的类型
5.类型变量Type可以是任意合法的名称
4.3.简化泛型函数的调用
function fn<Type>(val:Type):Type{return val}
// console.log(fn<number>(123),fn<object>({name:"张三"}))
//简化调用过程:类型推断
console.log(fn(123),fn({name:"张三"}))
1.在调用泛型函数时,可以省略类型来简化调用格式
2.此时ts内部会采用类型参数推断机制,根据实参自动推断出Type的类型
优点:代码更短,更易于阅读
缺点:当编译器无法推断类型或不准确时需要显示传入类型
4.4.泛型约束
4.4.1.使用场景,添加单个泛型类型变量并约束
默认情况下,泛型函数的类型变量Type可以代表多个类型,而这会导致无法访问任何属性,如:
//创建泛型函数
function fn<Type>(value:Type):Type{
console.log(val.length)
return value
}
//调用泛型函数
console.log(fn<number>(123))//比如number类型,就无法获取length属性
Type可以代表任意参数,但无法保证一定存在length属性,
解决方法:
- 方法一:执行更具体的类型
function fn<Type>(value:Type[]):void{console.log(value.length)}
fn<number>([1,2,3])//调用泛型函数:3
- 方法二:为泛型添加约束(进一步缩小类型的范围)
//创建泛型函数
interface ILen{length:number}
function fn<Type extends ILen>(value:Type):void{
console.log(value.length)
}
//调用泛型函数
fn<number[]>([1,2,3])//3
解释:
1.创建描述约束的接口ILen,该接口要求提供length属性
2.通过extends关键字使用该接口,为泛型类型变量添加约束
3.该约束表示:传入的实参类型必须具有length类型
4.4.2.添加多个泛型类型变量并约束
泛型的类型变量可以有多个,类型变量之间还可以约束,
如**:第二个类型变量受到第一个类型变量约束**
//创建一个函数来获取对象中属性的值
function getProp<Type,Key extends keyof Type>(obj:Type,key:Key){
return obj[key];
}
let obj = {name:'zhangsan',age:18};
console.log(getProp(obj,'name'));//zhangsan
console.log(getProp(obj,'age'));//18
解释:
1.添加第二个类型变量Key,用逗号分隔
2.keyof关键字:接受一个对象类型并生成其键名(string|number)的联合类型
3.keyof Type:获取obj所有键的联合类型,即:'name|age'
4.第二个类型变量Key受到第一个Type约束,理解:
Key只能是Type所有键的任意一个,只能访问对象中存在的属性
4.5.泛型+接口
接口也可以配合泛型来使用,增强灵活性和可复用性
//接口定义泛型
interface IGeneric<Type>{
id:(value:Type)=>Type//泛型函数
ids:()=>Type[]//泛型数组
}
//使用泛型接口
let obj:IGeneric<number>={//显式指定泛型类型为number
id:(value)=>{
return value;
},
ids:()=>[1,2,3]
}
解释:
1.在接口名称后IGeneric添加类型变量<Type>,这个接口就变成了泛型接口
2.接口的类型变量对接口中所有其他成员可见,
即:接口中所有成员都能使用类型变量
3.使用泛型接口时,需显示指定具体类型如number
4.此时id方法的参数与返回值的类型均为number;方法ids的返回值类型为number[]
4.6.验证:JS数组在TS中是一个泛型接口
//数组在TS中是一个泛型接口
const numArr: number[] = [1, 2, 3]
const strArr: Array<string> = ['1', '2', '3']
numArr.forEach((item) => {
console.log(item)
})
strArr.forEach((item) => {
console.log(item)
})
// 当我们在使用数组时,TS会根据数组类型自动将类型变量设置为相应的类型,
//验证:鼠标放到forEach上,会根据当前数组中的元素类型
//自动知悉当前数组接口对应的类型
//并且也会推断回调函数的前两个参数value和item的类型,并推断第三个参数array的类型
4.7.泛型+类
class也可以配合泛型来使用
//创建泛型类
class GenericNumber<Type>{
defaultValue: Type
add: (x:Type,y:Type)=>Type
}
//与泛型接口类似,在class名后面添加<Type>,就变成了泛型类
//add方法采用的是类似箭头函数的类型书写形式
//创建类实例:
const myNum = new GenericNumber<number>()//也与泛型接口类似,在创建class实例时,在类名后通过<number>来指定明确类型
myNum.defaultValue=0
4.8.泛型工具类型
TS内置了一些常用的工具类型,用于简化TS的一些常见操作
它们都是基于泛型来实现的,由于是内置的,所以可以直接在代码中使用
如:Partial<Type>,ReadOnly<Type>,Pick<Type,Key>,Record<Keys,Type>
4.8.1.Partial<Type>
用于构造(创建)一个类型,将Type的所有属性设置为可选
interface Person{
name:string
children:number[]
}
type PersonPartial=Partial<Person>
//构造出来的新类型PersonPartial和Person相同,但所有属性都变成可选的
let p1:Person={
name:'张三',
childred:[101,102,103]
}
let p2:PersonProps={}
4.8.2.ReadOnly<Type>
用于构造一个类型,将Type的所有属性都设置为只读
interface Person{
name:string
children:number[]
}
type PersonReadOnly=ReadOnly<Person>
//构造出来的新类型PersonReadOnly和Person相同,但所有属性都变成只读的
let p1:PersonReadOnly={
name:'张三',
childred:[101,102,103]
}
p1.name='李四'//报错
4.8.3.Pick<Type,Key>
从Type中选择一组属性来构造新类型
interface Person{
name:string
age:number
children:number[]
}
type PersonReadOnly=Pick<Person,'name'|'children'>
//两个类型变量,Person:选择谁的属性,'name'|'children':选择哪几个属性
let p1:PersonReadOnly={
name:'张三',
childred:[101,102,103]
}
4.8.4.Record<Keys,Type>
构造一个对象类型,属性键为keys,属性类型为Type
type PersonObj=Record<'name'|'sex',string>
let p=PersonObj={
name:'张三',
sex:'male'
}
//两个类型变量,'name'|'sex':对象有哪些属性,string:对象属性的类型
5.索引签名类型
绝大多数情况下,我们都能在使用对象前就确定对象的结构,并为对象添加准确的类型,
但当无法确定对象有哪些属性,或对象可以出现任意多个属性
时,需要使用到索引签名类型
interface AnyObject{
[key: string]:number
}
let obj1=AnyObject={
a:1,
b:2
}
解释:
1.使用[key:string]来约束接口中允许出现的属性名称为string类型
2.对象obj中可以出现任意多个属性
3.key只是一个占位符,可以换成任意合法的变量名称
//示例2:索引签名类型
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
// 输出:
console.log(myArray[0]);//输出:Bob
6.映射类型
6.1.使用场景
基于一个旧类型来创建新的对象类型,可以减少重复和提升开发效率
type PropKeys='x'|'y'|'z'
type Type={x:number;y:number;z:number}
//上述代码可以使用映射来简化如下:
type PropKeys='x'|'y'|'z'
type Type={[Key in PropKeys]:number}
解释:
1.映射类型是基于索引签名类型的,所以语法也类似,也使用了[]
2.Key inPropKeys表示key可以是PropKeys联合类型中的任意一个,类似于for(let key in obj)
3.使用映射类型创建的新对象类型Type和上面的完全相同
4.映射类型只能在类型别名上使用,不能在接口中使用
interface Type={[Key in PropKeys]:number}//报错
6.2.映射类型的keyof
映射类型除了可以根据联合类型创建新类型之外,还能根据对象类型来创建
type Props={a:number;b:string;c:boolean}
type Type={[key in keyof Props]: number}
解释:
1.首先,先执行keyof Props,获取到对象类型Props中所有键的联合类型abc
2.然后,Key in ...表示Key可以是abc中的任意一个,但是类型都是number了
6.3.验证:泛型工具类型都是基于映射类型实现的
以 Partial<Type>
为例:
- 回顾
Partial<Type>
的使用:
type Person{
name:string
children:number[]
}
type PersonPartial=Partial<Person>
- 鼠标悬停在
Partial<Person>
并CTRL+鼠标左键进入,复制代码
//粘贴代码:得到Partial的类型
type Partial<T>={
[P in keyof T]?:T[P];
}
解释:
1.keyof T就是keyof Person,可以获取Person的所有键,即'name'和'children'
2.在[]后添加问号表示这些属性是可选的,一次来实现Partial的功能
3.冒号后面的T[P]表示获取T中每个键对应的类型,如name的类型是string,children的类型是number[]
4.最终新类型Personpartial和旧类型Person结构完全相同,只是让所有类型变成可选的
6.4.索引查询类型
映射类型中提到的T[P]
语法被称为索引查询类型
也叫索引访问类型,作用是用来查询属性的类型
6.4.1.索引查询类型查询单个参数
type Person{//视频里是type不是interface
name:string
children:number[]
}
type PersonA=Person['name']//此时PersonA的类型是string
//[]中的属性必须存在于被查询类型中
6.4.2.索引查询类型查询多个参数
type PersonB=Person['name'| 'children']
//或者使用keyof操作符
type PersonC=Person[keyof Person]//string number[]
四.TypeScript类型声明文件
0.概述:类型声明文件及其作用
几乎所有JS应用都会引入许多第三方的库来完成任务需求,
这些第三方的库不管是否用TS编写,最终都要编译成JS代码
由于TS提供了类型才有了代码提示和类型保护等机制
但在使用第三方库时,我们发现它们都会有相应的TS类型,
这些类型是怎么来的?
使用到了类型声明文件,用来为已存在的js库提供类型信息
这样在TS项目中使用这些库时,就像用TS一样有代码提示和类型保护了
1.TypeScript的两种文件类型:.ts
和.d.ts
.ts
文件是代码实现文件
特点:
a.既包含类型信息又包含可执行代码
b.能被编译为.js文件然后执行
用途:编写程序代码
.d.ts
文件是类型声明文件
特点:
a.只包含类型信息的类型信息文件
b.不会生成js文件,仅仅用于提供类型信息
用途:为JS提供类型信息
2.类型声明文件(.d.ts
)的使用
2.1.使用已有的类型声明文件
2.1.1.内置的类型声明文件
ts为js运行时可用的所有标准化内置API都提供了声明文件
如:数组的所有方法都会有相应的代码提示和类型信息,这些实际上都是TS提供的内置类型声明文件
(在forEach上悬停,然后CTRL+左击跳转并查看lib.es5.d.ts
类型声明文件)
2.1.2.第三方库的类型声明文件
目前几乎所有常用的第三方库都有相应的类型声明文件,这些文件有两种存在形式:
- 库自带类型声明文件
安装axios后的目录 : node_modules/lib/index.d.ts
//这种情况下正常导入该库,TS就会自动加载库自己的类型声明文件,以提供该库的类型声明
- 由DefinitelyTyped提供
DefinitelyTyped是一个git仓库,用来提供高质量的TS类型声明
可以通过npm/yarn来下载该仓库提供的TS类型声明包,这些包的名称格式为:@types/xxx
,
比如:@types/lodash
在实际项目开发中,如果你使用的第三方库没有自带的声明文件,vscode会给出明确的提示如下:
尝试使用`npm i --save-dev @types/lodash`(如果存在),或者添加一个包含
`declare module 'lodash';`的新声明(.d.ts)文件
当安装@types/xx
x类型声明包后,TS也会自动加载该类型声明包,以提供该库的类型声明
此外,TS官方文档也提供了一个页面来查询@types/xxx库
2.2.创建自定义的类型声明文件
在开发的时候有以下两种情况需要创建自己的类型声明文件:
2.2.1.项目内共享类型
如果多个.ts
文件中都用到同一个类型,此时可以创建一个.d.ts
文件提供该类型,实现类型共享
步骤:
- 创建
index.d.ts
- 创建需要共享的类型,并export导出
- 在需要使用共享类型的
.ts
文件中import 导入
//index.d.ts
type Props={x:number;y:number}
export Props
//hello.ts
import {} from './index.d.ts'
let p: Props={x:1,y:2}
2.2.2.为已有的JS文件提供类型声明
又能分为以下两种情况:
1).在将JS项目迁移到TS项目时,为了让已有的.js文件有类型声明
2).成为库作者,创建库给其他人使用
类型声明文件的编写和模块化方式相关,不同的模块化方式有不同的写法,
但是由于历史原因,JS模块化的发展经过历经多种变化(AMD,CommonJS,UMD,ESModule等等),而TS支持各种模块化形式的类型声明
----这就导致类型声明文件的相关内容又多又杂
1).为已有的js项目提供类型声明
- 显然,TS项目中也可以使用
.js
文件,具体是在导入.js
文件时,TS会自动加载同名的.d.js
文件,已提供类型声明
使用到了declare关键字,用于类型声明(而非创建一个新变量)- 1.对于type,interface等这些明确就是TS类型的可以省略关键字
- 2.对于let,function等在TS和JS都能用的必须使用declare关键字,明确指定此处用于类型声明
//utils.js
let count=10
let song="跳楼机"
let position={
x=0,
y=0
}
function add(x,y){return x+y}
function changeDirection(dir){console.log(dir)}
function formatPoint= point=>{console.log(point)}
export { count , song , position , add , changeDirection ,formatPoint}
//新建utils.d.ts为同名js文件提供类型声明
//为已存在的变量提供类型声明
//必须使用关键字
declare let count: number
declare let song: string
//可以省略关键字
interface Point{//因为复用性,可以同时在js中的position和point中使用
x:number
y:number
}
declare let position:Point
//两种函数的写法分别对应的声明方式:
declare function add(x:number,y:number):number
declare function changeDirection(dir:'up'|'down'|'right'|'left'):void
type FormatPoint=(point:Point)=>void
declare const formatPoint:FormatPoint
//类型声明文件的书写和模块化方案有关,因此类型提供完毕后需要使用毛仰望方案中提供的语法(export)导出,才能在.ts文件中使用
//js文件中已使用了es6中的方案:export
export { count , song , position , add , changeDirection ,formatPoint}
//utils.ts
import { count , song , position , add , changeDirection ,formatPoint} from 'utils'
console.log(count,song,add(1,5))
2).成为库作者
略