TypeScript
TypeScript = JavsScript + 类型检测 + 先进的新特性
这里所说的先进的新特性,指的是一些js的新特性[es6,es7…]或者还没发布的新特性,也就是说ts和js是基本持平的甚至领先于js
TypeScript的编译环境
为了使用TS,我们就需要先安装 TS然后使用TS的compiler,将TS转化为浏览器可执行的JS
//全局安装TS,此时里面就自带了将TS转化为 JS的compiler
npm install typescript -g
//使用tsc 进行ts文件的编译。如果类型错误,则会编译不成功。
//例如
tsc index.ts //这样就会生成一个js,
//之后我们就可以和以前使用js文件一样使用
TypeScript 的运行环境
如果每次我们都向上面那样,使用tsc 进行ts文件->js文件的编译,然后再将js文件插入到浏览器或者node中执行的话也太麻烦了。
传统步骤:
第一步:通过tsc编译TypeScript到JavaScript代码;
第二步:在浏览器或者Node环境下运行JavaScript代码;
如何简化传统的步骤呢?换句话说我们如何在编写了ts代码之后直接运行在浏览器或者node上面了?
其实就是要寻求一个可以将上面两个步骤自动化的方法。
目前有两种方式,
一种是使用webpack配置一个自动化的环境,然后运行到浏览器上面。
一种是使用ts-node的库,相当于提供了一个可以运行ts文件的node环境。
搭建webpack执行ts的环境
使用ts-node的执行ts文件
这种方式就是和使用node执行ts文件一样。
//首先是安装ts-node
npm install ts-node -g
//另外ts-node需要依赖 tslib 和 @types/node 两个包
npm install tslib @types/node -g
//开始使用 ,和node一样的使用方式
ts-node math.ts
语法
Js中类型变量的声明
TS的主要作用就是进行类型检测,所以说,我们在定义变量的时候就需要指定标识符的类型。
声明类型后ts就会进行类型检测,声明的类型可以称之为类型注解;
//完整的声明如下
var/let/const 标识符: 数据类型 = 赋值;
例如:
let s:string = 'hello ts';
类型注解:string 和String 的区别
小写的才是正确的string 类型
大写的String 是string的包装类。
所以说如果我们要声明一个string的时候,就是使用小写的。其它的number等也是一样的
类型推导
默认情况下进行赋值的时候,会将赋值的值的类型,作为前面标识符的类型。这个过程就是类型推导。
如果没有进行赋值,则就无法进行类型推导,此时一定要写类型注解,不然会报错的。
也就是说如果我们在ts文件中定义了
let s = 'hello ts'
//其实默认就是
let s:string = 'hello ts';
所有默认情况下,如果可以推导出对应标识符的类型的时候,则可以不加类型注解。
基础数据类型的声明
上面我们只是以字符串类型进行了 ,一个变量是如何声明的,下面我们就讲下在ts中 有哪些数据类型。我们都知道ts是js的超集,所有JS中的所有数据类型在ts中都是有的。
基础数据类型的定义方式
//针对基础数据类型,我们如果定义变量
number
let num = 11; //类型推导直接定义
boolean
let flag = true; //类型推导直接定义
string
let s = 'hello'; //类型推导直接定义
symbol
let s1:symbol = Symbol('s1');
const obj = {
[s1]: 'abc',
}
//上面三个基础数据类型,编译器都可以推断出来。
//所以我们可以不要显示的进行类型注解
//但是null和underfined推断出来的是 any类型的。
//如果我们确定这个变量是null 或者是underfined 则最好显示的进行类型注解
null 类型的变量执行是null
undefined 类型的变量只能是undefined
let n: null = null
let d: undefined = null
下面显示了null和undefined没有进行类型注解时候,类型推断帮我们推断的结果。
数组的类型变量的声明
数组需要声明数组里面元素的类型
//数组类型的定义方式,数组类型的定义方式,有两种定义方式
//方式一,推荐使用
const arr: string[] = []; //表示定义一个string类型的数组【推荐使用】
//方式二,不推荐使用
const arr1: Array<string> = [] //同样是表示定义一个string类型的数组
//联合类型声明的数组 (string||number)[]
const arr:(string||number)[] = []
const arr1: Array<string> = []
不推荐使用的原因是,这样定义可能会和jsx中的标签冲突。 其实推荐的方法,const arr: string[] = []
和java中是类似的。java中定义数组是int[] arr = new Array();
可以看到只是变量名和类型注解前后位置调换了下。
object类型变量的声明
我们可以通过object这个对象类型对一个对象进行描述。但是用这种方法描述的对象,我们是无法获取和设置其中的值的? 我就不懂了这又什么意义呢?
意义:用于
例如:
let obj:object; // 定义了一个只能保存对象的变量
// obj = 1; //报错
// obj = "123"; //报错
// obj = true; //报错
obj = {name:'zzlw', age:18};
console.log(obj); //{ a: 1, b: 2 }
console.log(obj.a) //语法就报错了,显示类型object上不存在属性a
console.log(obj['a']) //编译报错
结论:声明为object对象类型的变量,只能用来接收和传递这个对象不能获取和设置里面的属性值。
函数的参数定义
在JS中函数是非常重要的组成部分,TS中允许我们指定函数的参数和返回值的类型。
普通函数的参数的类型注解
在声明函数的形参,我们可以在每个参数后面类型注解,以表明,声明函数参数所能接收的类型;并且在TS中,你声明了几个参数,你就必须传入符合类型的参数数量。数量不对,类型不对都会报错的。
// 给参数加上类型注解: num1: number, num2: number
// 给返回值加上类型注释: (): number
// 在开发中,通常情况下可以不写返回值的类型(自动推导)
function sum(num1: number, num2: number) {
return num1 + num2
}
一些回调函数的参数类型
在JS中有一些是内置的方法,例如数组的forEach,Map等方法,我们在使用的时候需要传入回调函数,此时的回调这个回调函数,我们是可以写类型的,因为TS会自动的推导出正确的类型。
const arr: number[] = [1, 2, 3, 4, 5]
// item根据上下文的环境推导出来的, 这个时候可以不添加的类型注解
// 上下文中的函数: 可以不添加类型注解
const arr1 = arr.map((item) => item)
//下图可以看到item 会被自动的推导出是number类型的。
可选的类型
可选类型指定的是这个属性是可选的,可以有也可以没有。
其实可选类型是 类型 | undefined
,这样的联合类型的语法糖。
可选类型是一个操作符, ?:
,可以被用在任何地方,下面以一个函数举例。
function fuc(a: number, b?: string) {
console.log(a) //1
console.log(b) //undefined
}
fuc(1)
如果对于可选类型,我们不进行传值的话,默认的值是 undefined
function fuc(a: number, b?: string) {
console.log(a)
console.log(b)
}
fuc(1, null)
//报错 ,我们知道在ts中undefiend类型只有一个值,那就是undefined,所以null不行
//Argument of type 'null' is not assignable to parameter of type 'string | undefined'.
联合类型
TypeScript的类型系统允许我们使用多种运算符,从现有类型中构建新类型。
- 我们来使用第一种组合类型的方法:联合类型(Union Type)
- 联合类型是由两个或者多个其他类型组成的类型;
- 表示可以是这些类型中的任何一个值;
- 联合类型中的每一个类型被称之为联合成员(union’s members);
```typescript const a: string | number = ‘1’;
在使用联合类型的时候,比如,函数的参数是联合类型的,那么我们需要在函数题里面,进行缩小(narrow)。这样才能正确的使用一些,类型的特用方法。
```typescript
// number|string 联合类型
function printID(id: number|string|boolean) {
// 使用联合类型的值时, 需要特别的小心
// narrow: 缩小
if (typeof id === 'string') {
// TypeScript帮助确定id一定是string类型,
console.log(id.toUpperCase())
} else {
console.log(id)
}
}
printID(123)
printID("abc")
printID(true)
默认参数
和ES6中是一样的,在ts中也可以给函数的参数设置默认参数,用法如下
function foo(x: number, y: number = 6): number {
return x + y
}
剩余参数的类型
在ES6中,引入了剩余参数的概念,剩余参数其实就是一个数组,所以我们给剩余参数定义类型其实就是定义一个数组的类型。
//定义一个number类型的 剩余参数的
function foo(x: number, y: number = 6, ...args: number[]): number {
return [x, y, ...args].reduce((pre, val) => pre += val)
}
console.log(foo(1, 2, 3, 4, 5, 6, 7))
函数的重载
在TS中,也存在函数重载的改变,但是相较于java中的函数重载,我觉得TS的函数重载略显鸡肋。
在TS中,实现重载,不是在这个重载的函数中,写函数体,而是需要另外一个通用的函数中写函数体。
其实这样是不能达到java中,那种自动选择参数类型符合的函数进行执行对于的函数体。下面已例子进行说明:
//重载不能写函数体,只能声明
function sum(num1: number, num2: number): number
function sum(num1: string, num2: string): string
// 实际执行的通用函数 。也就是参数类型不同时,执行的函数体是一样的。
// 这样用什么用呢?一般我们参数类型不同,执行的操作应该是不一样的吧。
//也就是说我们还要在通用函数内进行类似缩小。
function sum(num1: any, num2: any):any {
return num1 + num2
}
console.log(sum('1', '2')) //'12'
console.log(sum(1, 2)) //3
使用函数重载,我们需要声明 n个不带函数体的函数 + 1个具有函数体的具体执行的函数
总结:函数重载有点鸡肋,还不如参数使用联合类型,然后在函数体里面进行类似缩小。缺点就是返回值的类型是不能确定的
函数作为参数要定义函数类型
在定义函数的时候,我们会给函数的参数设置类型和函数的返回值设置类似。其实默认ts也会给我们函数名一个函数类型。应该就是类型推导的结果吧。
又因为在js中,函数作为第一等公民,我们可以将函数作为参数,也可以将函数作为返回值,所以这样我们就衍生出了一个问题,我们如何给这个参数设置为函数类型呢?
//1. 函数作为参数,在参数中如何编写为函数类型
//下表表示定于参数fn必须是一个没有参数的函数。
//声明为函数类型: ()=> void 这个函数没有参数,如果传入函数有参数会报错
function foo(fn: () => void):void {
return fn()
}
function bar() {
return 111
}
console.log(foo(bar)) //这里其实有bug ,这里能拿到 111的。我们将foo的返回值设置为void,也可以拿到一个number的返回值。
// 2.定义常量时, 编写函数的类型。其实和其它遍历定义类型是一样的。
//下面我们使用type 来定义一个比较难的函数类型,包括每个参数的类型和返回值的类型
type AddFnType = (num1: number, num2: number) => number
const add: AddFnType = (a1: number, a2: number) => {
return a1 + a2
}
TS中特有的数据类型
元组类型的声明
元组类型是严格限制了元组长度和每个元素类型的数组。也就是一个萝卜一个坑。
元组类型用于保存定长定数据类型的数据。
//元组类型声明的语法
let 元组名:[类型,类型] ; //在中括号内声明类型。
let arr5:[string, number, boolean];
// 表示定义了一个名称叫做arr5的元祖,
//这个元祖中将来可以存储3个元素,
//第一个元素必须是字符串类型,
//第二个元素必须是数字类型,
//第三个元素必须是布尔类型
arr5 = ['a', 1, true]; //一定只能这么赋值进去,不能一个个push,
//即使push的时候类型正确
// arr5 = ['a', 1, true, false]; // 超过指定的长度会报错
console.log(arr5);
any类型的声明
any表示任意类型, 当我们不清楚某个值的具体类型的时候我们就可以使用any,任何数据类型的值都可以赋值给any类型
一般用于定义一些通用性比较强的变量, 或者用于保存从其它框架中获取的不确定类型的值
注意不要过多使用any,因为什么都是any那ts就变成js了
let value:any; // 定义了一个可以保存任意类型数据的变量
value = 123;
value = "abc";
value = true;
value = [1, 3, 5];
void类型的声明
void和any正好相反,表示没有任何类型,一般用于函数的返回值。在TS中只有null和undefined可以赋值给void类型。因为null和undefined类型是所有类型的子类型。
function test():void {
console.log("hello world");
}
test();
let value:void; // 定义了一个不可以保存任意类型数据的变量, 只能保存null和undefined
// value = 123; // 报错
// value = "abc";// 报错
// value = true;// 报错
// 注意点: null和undefined是所有类型的子类型, 所以我们可以将null和undefined赋值给任意类型
// value = null; // 不会报错
value = undefined;// 不会报错
unkonwn类型的声明
unkonwn
类型是TS中一种比较特殊的类型,用来描述类型不确定的变量。
let result:unknown;
never类型的声明
never 表示永远不会发生值的类型,比如一个函数:
如果一个函数中是一个死循环或者抛出一个异常,那么这个函数会返回东西吗? 不会,那么写void类型或者其他类型作为返回值类型都不合适,我们就可以使用never类型;
字面量类型
在我眼里,无法通过类型推断出的类型,或者是显示的给他声明一个 “自定义类型”,的时候就是字面量类型。
这里的自定义类型,可以是任何的类型,string,number… 等等。下面主要是以string里举例,但是千万不要以为只有string类型定义的才是字面量类型
//自动推断为,为{a:1} 【他不是内置的类型吧】 类型
let obj = { a: 1 }
obj.b = 2
//编写错误:因为obj是一个{a:1} 类型的,我们无法给这个类型添加 b 属性
//类型“{ a: number; }”上不存在属性“b”
console.log(obj)
//显示的声明一个字面量类型 'iam a string'
let str: 'iam a string' = 'iam a string'
//编写错误:不能将类型“"sss"”分配给类型“"iam a string"”。
str = 'sss'
console.log(str)
//因为str可以自动推断出是string【内置存在的类型】类型的,所以str不是字面量类型的元素
let str = 'iam a string'
str = 'sss'
console.log(str)
// 下面这个同样是字面量类型
let num: 1 = 1;
字面量类型的使用
当字面量类型和联合类型一起使用的时候,那么我们就可以指定这个属性只有是指定值了。
type Alignment = 'left' | 'right' | 'center'
let align: Alignment = 'left'
align = 'right'
align = 'center'
function fc(direction: Alignment) {
//能传给direction 的使用是 'left' | 'right' | 'center' 中的一个
console.log('方向是:', direction)
}
fc(align)
注意: 字面量类型的 “center” 和值是字符串”center” 是不一样的。如下
type Alignment = 'left' | 'right' | 'center'
let align = 'center' //这里没有给align进行类型注释,ts自动推导出align是string的。
//但是它的值是'center'
function fc(direction: Alignment) {
console.log('方向是:', direction)
}
fc(align) //编写错误:类型“string”的参数不能赋给类型“Alignment”的参数。
//这里使用断言,使得align 可以传入函数
fc(align as 'center') //表明align是'center' 类型的
思考: 我们能不能这么说呢,我们使用字符串进行声明的字面量类型,其实是strign的子类型,只要符合,我们就可以 由 string (as) 字面量类型
同理声明一个函数的时候,也是会默认进行字面量类型的
//我们默认声明一个函数的时候,这个函数是有类型的
//例如,这样foo就是函数函数类型了
function foo(){
}
TS中一些关键字的使用
类型别名 type
在使用联合类型,或者对象类型的使用,如果相同的类型注解,每次我们都要写一长串的时候,我们就可以使用一个别名来表示这个类型的注解。
type posObj = {
x: number,
y: number,
}
//这里的obj指定了,posObj类型的,那么这个对象一定是只有x和y属性的,且属性值只能是number的
function fc(obj: posObj) {
console.log(obj.x)
console.log(obj.y)
}
fc({ x: 1, y: 2 })
fc({ x: 1, y: 2 ,z:3}) //直接报错,
//类型“{ x: number; y: number; z: number; }”的参数不能赋给类型“posObj”的参数。
//对象文字可以只指定已知属性,并且“z”不在类型“posObj”中。
类型断言 as
类型断言的作用就是将一个变量的类型,由 宽泛 -> 具体 / 未知 。
为什么需要转化为更加的具体呢?因为在一些更加具体的类型中,存在一些这个类型特有的方法,如果我们使用的是更宽泛的类型的话,那么是没有这个方法的。
//示例一:
class Person {
}
class Student extends Person {
learn() {
console.log('learning')
}
}
function fnc(obj: Person) {
// obj.learn() //报错:类型“Person”上不存在属性“learn”
(obj as Student).learn() //通过
}
fnc(new Student())
//示例二: DOM中更加的常见
//语法错误:
const img = document.querySelector('#my-img') //此时img是Element类型的
img.src = './avator.jpg' //错误,类型“Element”上不存在属性“src”。
const img = document.querySelector('#my-img') as HTMLImageElement
img.src = './avator.jpg'
//方式三:使用断言可以将一个变量从一个类型转化到其它的类型。
//原理是:具体 -> 未知 -> 具体
//as any/unknown
//还是不要这样操作吧。
const message = "Hello World"
let num: number = (message as unknown) as number
num = 1
console.log(num) //1
非空类型断言 !
非空类型断言的作用是表示某些值我们一定是有传入值的时候使用的。
通常是是在使用了可选类型的时候,如果我们确定这个可选参数是有值的情况下,我们可以使用非空类型断言,不然的话,会出现编译阶段出错的情况。
//没有使用非空类型断言,编译器会觉得,你的mes可能是没有传入值的时候,所以给我们编译不通过
function fc(mes?: string) {
console.log(mes.toUpperCase())
}
fc('aaa')
//编写的时候,不会出现错误,但是当编译的使用。会报错,所mes is possibly 'undefined'
//使用非空类型断言
function fc(mes?: string) {
//使用非空类型断言,表示mes一定存在值
console.log(mes!.toUpperCase())
}
fc('aaa')
注意非空类型断言,和 ES11中的可选链。不要搞混了
类的定义
其实在JS开发过程了,我们更加习惯于基于函数式变成。
比如React开发中,目前更多使用的函数组件以及结合Hook的开发模式;
比如在Vue3开发中,目前也更加推崇使用 Composition API;
但是在封装某些业务的时候,类具有更强大封装性,所以我们也需要掌握它们
所以在TS中,我们主要是对类的属性和方法进行静态类型检测。
在类中定义了属性和方法之后,就没法再添加新的属性和方法了。类的类型一定固定了。
和JS中定义类不同的点主要是在class类中,我们必须先声明这个类的属性及其类型。而且如果没有构造函数给这个属性初始化的时候,我们还必须在声明这个属性的时候给他设置初始化的值。
class Person {
//没有构造函数在这里必须设置默认的值
name: string = 'zzlw'
age: number = 18
eating() {
console.log(this.name + " eating")
}
}
const p = new Person()
console.log(p.name)
console.log(p.age)
p.name = 'yaoye'
p.age = 0;
console.log(p.name)
console.log(p.age)
p.eating()
export { }
//输出:
zzlw
18
yaoye
0
yaoye eating
class Person {
name: string
age: number
//如果我们在这里有构造函数进行对每个属性进行赋值(相当于初始化),
//则属性声明的时候不需要设置默认值
constructor(name: string, age: number) {
//必须对没有设置默认值的属性,进行赋值。不然还是会报没有初始化的错误
this.name = name;
this.age = age;
}
eating() {
console.log(this.name + " eating")
}
}
const p = new Person('zzlw', 18)
console.log(p.name)
console.log(p.age)
p.name = 'yaoye'
p.age = 0;
console.log(p.name)
console.log(p.age)
p.eating()
export { }
//输出:
zzlw
18
yaoye
0
yaoye eating
类的继承
在子类的构造函数中调用super()
,对父类的属性进行初始化。
在子类中可以重写父类的方法,如果要调用父类的方法同样可以使用super.方法名()
来调用父类的方法,任何方法都可以的噢。
class Person {
name: string
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
eating() {
console.log("eating 100行")
}
}
class Student extends Person {
sno: number
constructor(name: string, age: number, sno: number) {
// super调用父类的构造器
super(name, age)
this.sno = sno
}
eating() {
console.log("student eating")
//调用父类的方法。所有的父类方法都可以调用。不单单是重写的
super.eating()
}
studying() {
console.log("studying")
}
}
const stu = new Student("why", 18, 111)
console.log(stu.name)
console.log(stu.age)
console.log(stu.sno)
stu.eating()
export {}
类的成员的修饰符
在TypeScript中,类的属性和方法支持三种修饰符: public、private、protected
- public 修饰的是在任何地方可见、公有的属性或方法,默认编写的属性就是public的;
- private 修饰的是仅在同一个类中可见、私有的属性或方法; 如果是声明私有方法的化,也可以使用
#
- protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法;
如果进行方法的重载的化,则父类子类的方法都要是public 的。不然好像会报错。
自读属性readonly
如果有一个属我们不希望外界可以任意的修改,只希望确认值之后可以直接使用,而不可以修改。
class Student {
readonly name: string;
constructor(name: string) {
this.name = name;
}
action() {
console.log(this.name)
}
}
const stu1 = new Student('zzlw');
stu1.action()
console.log(stu1.name)
stu1.name = 'jalen'
//解析就报错
//无法分配到 "name" ,因为它是只读属性。
getters/setters
gettesr和setters是存储器,主要是用来对私有属性的获取(getters)和设置(setters)进行监听。
常用方式如下,我们对私有属性声明一个[临时]变量_变量名
,然后通过存储器进行操作,其实都是对这个[临时]变量进行操作。
class Student {
private _name: string;
constructor(name: string) {
this._name = name;
}
get name() {
//这里可以做其它的
return this._name
}
set name(newVal) {
//这里也可以做其它的
this._name = newVal
}
}
const stu1 = new Student('zzlw');
console.log(stu1.name)
stu1.name = 'jalen'
console.log(stu1.name)
静态成员
如果我们需要定义一些静态的属性和方法,则是使用static
关键字进行定义。
定义的静态属性必须通过类名.静态属性
的方式进行访问。 无论是在类的内部,还是外面。
定义的静态方法一样的也是通过类名.方法名()
的方式进行调用。
class Student {
static nickName: string = 'zzlw';
static change() {
console.log(111)
console.log(Student.nickName)
}
}
const stu1 = new Student();
console.log(Student.nickName)
Student.change()
//输出
zzlw
111
zzlw
抽象类abstract
抽象类的作用:
我们在定义很多接口的时候,因为传入的参数类的类型可能有很多的不同,所以我们通常会将这些类抽象出一个类作为它们的父类。在这个父类中,声明了这些类的公用方法但是没有实现,这就叫做抽象方法。
什么是抽象方法?
抽象方法,必须存在于抽象类中
抽象类和抽象方法都要加上 abstract 关键字
抽象类的特点?
抽象类不能被实例化(不能被new)
抽象方法必须被子类实现,如果子类没有实现抽象方法,则这个子类必须是抽象类。
abstract class Shape {
// 在抽象类中,主要是定义一些抽象方法,这些抽象方法没有函数体的。抽象方法是给实现类中实现的
abstract getArea(): number
}
class Circle extends Shape {
private r: number;
constructor(r: number) {
// 如果实现类,有构造方法,则在构造方法中一定要调用super()
super()
this.r = r
}
getArea(): number {
return this.r ** 2 * 3.14
}
}
class quadrilateral extends Shape {
private w: number;
private h: number;
constructor(w: number, h: number) {
// 如果实现类,有构造方法,则在构造方法中一定要调用super()
super();
this.w = w;
this.h = h;
}
getArea(): number {
return this.w * this.h
}
}
// 类也可以作为类型的
function getArea(shape: Shape): number {
return shape.getArea()
}
console.log(getArea(new Circle(2)))
console.log(getArea(new quadrilateral(2, 3)))
接口
给对象使用接口定义的类型
接口作为普通的对象的类型
接口的作用类似于 使用类型别名(type) 声明指定的对象的类型。
定义接口的时候,我们通常将这个接口的第一个自己定义为I
表示这是一个接口
在接口中,同样可以使用readonly
关键字和可选类型 ?:
//定义接口的时候,我们通常将这个接口的第一个自己定义为I 表示这是一个接口
interface Iperson1 {
name: string,
age: number,
friends?: {
name: string,
age: number
}
}
type person2 = {
name: string,
age: number,
friends?: {
name: string,
age: number
}
}
const p1: Iperson1 = {
name: 'zzlw',
age: 18,
friends: {
name: 'wcx',
age: 18,
}
}
const p2: person2 = {
name: 'zzlw',
age: 18,
friends: {
name: 'wcx',
age: 19,
}
}
console.log(p1)
console.log(p2)
//输出
{ name: 'zzlw', age: 18, friends: { name: 'wcx', age: 18 } }
{ name: 'zzlw', age: 18, friends: { name: 'wcx', age: 19 } }
接口声明对象的key是索引类型。
// 通过interface来定义索引类型
// 这个索引类型不经可以用于对象,还可以用于数组,主要索引是numbe的化
interface IndexLanguage {
[index: number]: string
}
// 因为indexLanguage的所有是数字的,所以数组和对象都可以运用这个接口作为自己自己的类型
const frontLanguage: IndexLanguage = [
"HTML",
"CSS",
"JavaScript",
"Vue"
]
interface ILanguageYear {
[name: string]: number
}
// ILanguageYear 的索引是字符串,所以这里只能作为对象的类型
const languageYear: ILanguageYear = {
"C": 1972,
"Java": 1995,
"JavaScript": 1996,
"TypeScript": 2014
}
console.log(frontLanguage)
console.log(languageYear)
给函数使用接口定义的类型
不要用这种,用type不香吗
给函数定义类型主要是给表达式函数使用的。显示声明的函数已经使用function声明了 这个”字母”表示函数了呀。
给一个变量名声明为函数,主要是用来限制调用这个函数的使用要传入的参数类型和个数,而不会显示实际这个函数要接收多少参数。下面这个例子就体现了。也就是说,我要你这么传,但是我可以不这么接收。
interface Isum {
(num1: number, num2: number): void
}
// 这里设置函数的类型是用来限制,调用的。而没有限制到函数自己。
// 也就是说,在我这里例子中,sum函数既是没有使用两个形参也是可以的。
const sum: Isum = (...args: number[]) => {
console.log(...args)
}
// 因为sum定义了类型,所以这里我们必须要传入两个参数。
sum(0, 1)
接口的继承
接口和类一样是可以继承的,也是通过使用extends
关键字来实现的。并且接口相较于类,是支持实现多继承的。
interface ISwim {
swimming: () => void
}
interface IFly {
flying: () => void
}
interface IAction extends ISwim, IFly {
}
const action: IAction = {
swimming() {
},
flying() {
}
}
接口的实现
和java中一样,接口也是可以被类实现的,实现的类也就表明了将接口中的所有属性和方法都实现,好处就是在需要传入接口的地方,都可以将接口的实现类传入。这就是面向接口进行开发。
interface ISwim {
swimming: () => void
}
interface IEat {
eating: () => void
}
// 类实现接口
class Animal {
}
// 继承: 只能实现单继承
// 实现: 实现接口, 类可以实现多个接口
class Fish extends Animal implements ISwim, IEat {
swimming() {
console.log("Fish Swmming")
}
eating() {
console.log("Fish Eating")
}
}
class Person implements ISwim {
swimming() {
console.log("Person Swimming")
}
}
// 编写一些公共的API: 面向接口编程
function swimAction(swimable: ISwim) {
swimable.swimming()
}
// 1.所有实现了接口的类对应的对象, 都是可以传入
swimAction(new Fish())
swimAction(new Person())
swimAction({ swimming: function () { console.log('普通对象') } })
交叉类型
我们都知道联合类型是或的关系,而交叉类型就是与的关系。所以说交叉类型通常被用于对象类型的定义中。如果是普通类型进行交叉,不就没意义了吗?例如type nType = number & string
这样不就变成了never类型了吗,哈哈。
interface Iswing {
swing: () => void
}
interface Ieating {
eating: () => void
}
type action = Ieating & Iswing
const obj: action = {
swing() {
console.log('xixi')
},
eating() {
console.log("huohuo")
}
}
obj.eating()
obj.swing()
interface和type的区别
可以看到interface
和type
的作用是类似的,都可以用来给变量【这里的变量指所有东西】定义类型使用,那么何时使用接口何时使用type呢?
- 定义非对象类的时候,使用type
- 定义对象类型的使用,使用interface。因为interface可以重名,默认会进行交叉类型,可以重复的对某个接口来定义属性和方法; 而type定义的是别名,别名是不能重复的
//接口重名,它会默认包含全部的,如果在两个接口中,对一个属性名冲突,则在会直接在定义的时候就报错
interface Iswing {
swing: () => void
}
interface Iswing {
age: number,
swing: () => void
}
const obj: Iswing = {
age: 18,
swing() {
console.log('xixi')
},
}
字面量赋值【重要】
现在假设我们有一个对象,一个类型,和一个规定了参数类型的函数【用来接收这个对象】。
因为接收函数的形参是规定了类型的、所以如果我们直接将对象传入这个函数是会报错的。但是如果我们将这个对象传给一个的变量,此时进行类型推导的时候就会进行freshness,然后再将这个变量传递到函数中,此时就不会报错。
interface IPerson {
name: string
age: number
height: number
}
// const info = {
// name: "why",
// age: 18,
// height: 1.88,
// address: "广州市"
// }
// // freshness擦除
// const p: IPerson = info
// console.log(info)
// console.log(p)
function printInfo(person: IPerson) {
console.log(person)
}
// 代码会报错
// printInfo({
// name: "why",
// age: 18,
// height: 1.88,
// address: "广州市"
// })
//此时会进行类型推导,
const info = {
name: "why",
age: 18,
height: 1.88,
address: "广州市"
}
printInfo(info)
枚举类型
枚举类型其实就是将一组可能出现的指,一个个的列举出来,定义在一个类型中,这个类型就是枚举类型。【主要使用来定义一些常量表示一些值使用的吧】
枚举允许开发者定义一组命名常量,常量可以是数字、字符串类型;
// 定义一个枚举类型
enum Direction {
LEFT,
RIGHT,
TOP,
BOTTOM
}
// 使用的话,直接 . 出来就好了
// 其实枚举里面的值,默认是从0开始的值。关注枚举的值,意义不大其实
console.log(Direction.LEFT) //0
console.log(Direction.RIGHT) //1
//当然我们也可以显示的给枚举的值进行更改,之后的枚举元素的值会在这个之后递增
// 定义一个枚举类型
enum Direction {
LEFT,
RIGHT = 2,
TOP,
BOTTOM = 7,
CENTER
}
// 使用的话,直接 .出来就好了
// 其实枚举里面的值,默认是从0开始的值
console.log(Direction.LEFT) //0
console.log(Direction.TOP) //3
console.log(Direction.BOTTOM) //7
泛型
对函数的参数使用泛型
泛型的作用:给类型实现参数化。也就是说,我们不要在一开始就把这个类型定死了。而是在使用的时候,指定这个类型是什么类型的。
函数的泛型是写在,函数名后面的中括号里面。
下面给函数的形参设置为泛型,为例:
// 类型的参数化
// 在定义这个函数时, 我不决定这些参数的类型
// 而是让调用者以参数的形式告知,我这里的函数参数应该是什么类型
function fnc<T>(a: T) {
return a
}
console.log(fnc<number>(1))
console.log(fnc({ a: 1 })) //如果没有设置的话,也会进行类型的推导,
//在这里会推导出它们是 字面量类型的,因为字面量类型对于我们的函数也是适用
//此外,也可以同时定义多个泛型
function foo<T, E, O>(arg1: T, arg2: E, arg3?: O, ...args: T[]) {
}
foo<number, string, boolean>(10, "abc", true)
对接口使用泛型
在定义接口的时候,我们也可以使用泛型来指定接口中的某些属性的的类型。
接口的泛型写在的是接口的后面,在调用的时候,也是写在所使用作为类型的接口的后面。
// 泛型也可以设置默认值
interface P<T = number> {
name: string,
age: T,
eating: (num1: T) => void
}
//使用接口时,给变量设置接口,然后给接口设置泛型
const p1: P<string> = {
name: 'zzlw',
age: '18',
eating(s) {
console.log(s)
}
}
// 如果有
const p2: P = {
name: 'zzlw',
age: 18,
// 和之前一样,这里不接也没关系
eating() { }
}
p1.eating('1')
// 但是你一定要传符合的类型。
p2.eating(1)
对类使用泛型
class Point<T> {
x: T
y: T
z: T
constructor(x: T, y: T, z: T) {
this.x = x
this.y = y
this.z = y
}
}
//类: 类型推导
const p1 = new Point("1.33.2", "2.22.3", "4.22.1")
//类: 手动设置泛型
const p2 = new Point<string>("1.33.2", "2.22.3", "4.22.1")
// 普通变量设置类型,然后给类型设置泛型
const p3: Point<string> = new Point("1.33.2", "2.22.3", "4.22.1")
const names1: string[] = ["abc", "cba", "nba"]
const names2: Array<string> = ["abc", "cba", "nba"] // 不推荐(react jsx <>)
泛型约束【向上转型】
泛型约束作用:泛型表示可以传入不同的类型对吧?现在我们需要这些传入的类型都有共同的东西,就有可以给泛型进行约束。具体实现,就是泛型一个具体的接口,这个接口规定了这个共有的东西。
// 先定义一个接口,这个接口有我们公用的东西
interface IProperty {
length: number
}
// 泛型约束具体实现,泛型继承一个类型
function getLength<T extends IProperty>(a: T) {
return a.length
}
console.log(getLength("abc"))
console.log(getLength(["abc", "cba"]))
console.log(getLength({ length: 100 }))