这是我参与「第四届青训营 」笔记创作活动的的第4天
Typescript 如今运用的越来越广泛,在本次青训营中,深刻体会到了 ts 的魅力所在,于是,我详细的整理了一下 Typescript 的知识点。
js 和 ts 的区别
从编程语言的动静来区分,Typescript 属于静态类型的编程语言,JavaScript 属于动态类型的编程语言。
静态类型:编译期做类型检查 ; 动态类型:执行期做类型检查。
代码是先编译后执行。所以 typescript 能比 JavaScript 更早的发现错误。
ts 相比 js 的优势
- 更早(写代码的同时)发现错误,减少找 Bug ,改 Bug 事件,提升开发效率。
- 程序中任何位置的代码都有代码提示,随时随地的安全感,增强了开发体验。
- 强大的类型系统提升了代码的可维护性,使得重构代码更加的容易。
- TS 类型推断机制,不需要在代码的每个地方都显示标注类型,让你享受优势的同时,尽量降低了成本。
除此之外,Vue3 源码使用 TS 重写,Angular 默认支持 TS ,React 与 TS 完美配合, Typescript 已经成为大中型前端项目的首选编程语言。
搭建typescript学习环境
安装typescript
npm i -g typescript
查看是否安装成功
tsc -v
安装ts-node
npm i -g ts-node
类型注解
let age:number = 18
说明:代码中的 :number 就是类型注解。
作用:为变量添加类型约束。比如,上述代码中,约定变量 age 的类型为number(数值类型)。
解释:约定了什么类型,就只能给变量赋值该类型的值,否则,就会报错。
常用类型
原始类型
let str: string = "jimmy";
let num: number = 24;
let bool: boolean = false;
let u: undefined = undefined;
let n: null = null;
let obj: object = {x: 1};
let big: bigint = 100n;
let sym: symbol = Symbol("me");
注意:默认情况下
null和undefined是所有类型的子类型。就是说你可以吧null和undefined赋值给其他类型。
// null和undefined赋值给string
let str:string = "666";
str = null
str= undefined
// null和undefined赋值给number
let num:number = 666;
num = null
num= undefined
// null和undefined赋值给object
let obj:object ={};
obj = null
obj= undefined
// null和undefined赋值给Symbol
let sym: symbol = Symbol("me");
sym = null
sym= undefined
// null和undefined赋值给boolean
let isDone: boolean = false;
isDone = null
isDone= undefined
// null和undefined赋值给bigint
let big: bigint = 100n;
big = null
big= undefined
注意:虽然
number和bright都表示数字,但是这两个类型不兼容。
let big: bigint = 100n;
let num: number = 6;
big = num;
num = big;
这样会抛出一个类型不兼容的错误。
数组类型
数组类型的两种写法:
let numbers:number[] = [1,3,5]//推荐使用这种写法
let strings:Array<string> = ['a','b','c']
如果数组中既有number类型,又有string类型,可以这样写:
let arr:(number|string)[] = [1,'a',2,'b']
解释:|(竖线) 在 TS 中叫做联合类型(由两个或者多个其他类型组成的类型,表示可以是这些类型中的任意一种)。
注意:添不添加小括号,两者的意思完全不同
// 不添加小括号,表示:arr1既可以是 number 类型,又可以是 string 类型的数组
let arr1:number|string[] = ['a','b']
arr1 = 123
类型别名
类型别名(自定义类型):为任意类型的别名。
使用场景:当统一类型(复杂)被多次使用时,可以通过类型别名,简化该类型的使用。
type CustomArray = (number|string)[]
let arr1:CustomArray = [1,'a',3,'b']
let arr2:CustomArray = ['x','y',6,7]
解释:
- 使用 type 关键字来创建类型别名。
- 类型别名(比如,此处的 CustomArray ),可以是任意合法的变量名称。
- 创建类型别名后,直接 使用该类型别名作为变量等等类型注解 即可。
函数类型
函数的类型实际上指的是:函数参数和返回值的类型。
为函数指定类型的两种方式:1.单独指定参数,返回值的类型 2.同时指定参数,返回值的类型。
-
单独指定参数,返回值的类型
function add(num1:number,num2:number):number{ return num1+num2 }const add = (num1:number,num2:number):number=>{ return num1+num2 } -
同时指定参数,返回值类型:
const add:(num1:number,num2:number) => number = (num1,num2) =>{ return num1+num2 }解释:当函数作为表达式时,可以通过类似箭头函数形式的语法来为函数添加类型。
注意:这种形式只适用于函数表达式。
void类型
如果函数没有返回值,那么,函数返回值类型为:void
function greet(name:string):void{
console.log('Hello',name)
}
可选参数
使用函数实现某个功能时,参数可以传也可以不传。这种情况下,在给函数参数指定类型时,就用到可选参数了。
function myslice(start?:number,end?:number):void{
console.log('起始索引',start,'结束索引',end)
}
可选参数:在可传可不传的参数名称后面添加 ? (问号)。
注意:可选参数只能出现在参数列表的最后,也就是说可选参数后面不能再出现必选参数。
默认参数
function myslice(start:number,end:number = 1):void{
console.log('起始索引',start,'结束索引',end)
}
剩余参数
function push(array: any[], ...items: any[]) {
items.forEach(function(item) {
array.push(item);
});
}
let a = [];
push(a, 1, 2, 3);
对象类型
在 TS 中对象的类型就是在描述对象的结构(有什么类型的属性和方法)。
let person:{ name:string; age:number; sayHi():void } = {
name:'jack',
age:19,
sayHi(){}
}
解释:
- 直接使用 {} 来描述对象结构。属性采用属性名:类型的形式,方法采用方法名():返回值类型的形式。
- 如果方法有参数,就在方法名后面的小括号中指定参数类型(比如:greet(name:string):void)
- 在一行代码中指定对象的多个属性类型时,使用;(分号)来分隔。
-
如果一行代码只指定一个属性类型(通过换行来分隔多个属性类型),可以去掉 ;(分号).
-
方法的类型也可以使用箭头函数的形式(比如:{ sayHi:()=>void })。
let person:{ name:string age:number sayHi:()=>void } = { name:'jack', age:19, sayHi(){} }
可选属性
对象的属性或者方法,也可以是可选的,此时就用到可选属性了。
可选属性的语法与函数可选参数的语法一致,都使用 ? (问号) 来表示。
function my(person:{name:string;age?:number}){
console.log(person)
}
my({name:"芜湖"})
接口
当一个对象类型被多次使用时,一般会使用接口来描述对象的类型,达到复用的目的。
解释:
- 使用
interface关键字来声明接口 - 接口名称(比如,此处的IPerson) ,可以是任意合法的变量名称
- 接口声明后,直接使用接口作为变量的类型
- 因为每一行只有一个属性类型,因此,属性后面没有 ; (分号)
interface IPerson {
name:string
age:number
sayHi():void
}
let person:IPerson = {
name:'jack',
age:19,
sayHi(){}
}
interface(接口)和type(类型别名) 的对比
-
相同点:都可以给对象指定类型。
-
不同点:
- 接口,只能为对象指定类型。
- 类型别名,不仅可以为对象指定类型,实际上可以为任意类型指定别名
interface IPerson {
name:string
age:number
sayHi():void
}
type IPerson = {
name:string
age:number
sayHi():void
}
type NumStr = number | string
接口继承
如果两个接口之间有相同的属性或方法,可以 将公共的属性或方法抽离出来,通过继承来实现复用。
比如,这两个接口都有x,y两个属性,重复写会显得非常的繁琐。
interface Point2D { x:number;y:number }
interface Point3 { x:number;y:number;z:number }
更好的方法:
interface Point2D { x:number;y:number }
interface Point3 extends Point2D { z:number }
- 使用 extends (继承) 关键字实现了接口 Point3D 继承 Point2D。
- 继承后,Point3D 就有了 Point2D 的所有属性和方法(此时,Point3D同时有了x,y,z三个属性)
字面量类型
let str1 = 'hello ts'
const str2 = 'hello ts'
通过 TS 类型推倒机制,可以得到答案:
- 变量 str1 的类型为:string
- 变量 str2 的类型为:’hello ts‘
解释:
- str1 是一个变量(let),它的值可以是任意字符串,所以类型为:string。
- str2 是一个常量(const),它的值不能变化只能是’hello ts‘,所以,他的类型为’hello ts‘
注意:此处的 ’hello ts‘ ,就是一个字面量类型。也就是说某个特定的字符串也可以作为 TS 中的类型。
除字符串之外,任意的 JS 字面量(比如,对象,数字等)都可以作为类型使用。
let age:18 = 18//true
let age:19 = 19//false
使用模式:字面量类型配合联合类型一起使用。
使用场景:用来 表示一组明确的可选值列表。
比如,在贪吃蛇游戏中,游戏的方向的可选值只能是上,下,左,右中的任意一个。
function changeDirection(direction:'up'|'down'|'left'|'right'){
console.log(direction)
}
解释:参数 direction 的值只能是 up/down/left/right 中的任意一个。
优势:相比于 string 类型,使用字面量类型更加精确,严谨。
枚举
枚举的功能类似于字面量类型+联合类型组合功能,也可以表示一组明确的可选值。
枚举:定义一组命名常量。它描述一个值,该值可以是这些命名常量中的一个。
enum Direction {up,Down,Left,Right}
function changeDirection(direction:Direction){
console.log(direction)
}
changeDirection(Direction.up)
解释:
- 使用
enum关键字定义枚举。 - 约定枚举名称,枚举中的值以大写字母开头。
- 枚举中的多个值通过
,(逗号) 分隔。 - 定义好枚举后,直接使用枚举名称作为类型注解。
注意,形参 direction 的类型为枚举 Direction ,那么,实参的值就应该是枚举 Direction 成员的任意一个。
访问枚举成员,直接通过点
.语法进行访问。
枚举成员的值及数字枚举
当我们将鼠标移到 Direction.up ,可以看到枚举成员 Up 的值为 0。
注意:枚举成员是有值的,默认为:从 0 开始自增的数值。
我们把,枚举成员的值为数字的枚举,称为:数字枚举。
当然,也可以给枚举中的成员初始化值。
//Down -> 11,Left -> 12,Right ->13
enum Direction {up = 10,Down,Left,Right}
enum Direction {up = 2,Down = 4,Left = 8,Right = 16}
字符串枚举
字符串枚举:枚举成员的值是字符串。
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT'
}
注意:字符串枚举没有自增长行为,因此,字符串枚举的每个成员必须有初始值。
枚举的原理
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT'
}
上面这行代码,实际上在 JS 中会被编译成下面这样代码
var Direction;
(function (Direction) {
Direction["Up"] = "UP";
Direction["Down"] = "DOWN";
Direction["Left"] = "LEFT";
Direction["Right"] = "RIGHT";
})(Direction || (Direction = {}));
any
原则:不推荐使用 any 。(失去 TS 类型保护的优势)
首先: any 类型,允许被赋值为任意类型。
let a: any = 666;
a = "Semlinker";
a = false;
a = 66
a = undefined
a = null
a = []
a = {}
其次:在 any 上访问任何属性都是允许的,也允许调用任何方法。
let anyThing: any = 'hello';
console.log(anyThing.myName);
console.log(anyThing.myName.firstName);
let anyThing: any = 'Tom';
anyThing.setName('Jerry');
anyThing.setName('Jerry').sayHello();
anyThing.myName.setFirstName('Cat');
注意:有两种隐式具有 any 类型的情况:1. 声明变量不提供类型也不提供默认值 2. 函数参数不加类型
let age
function add(x,y){
return x+y
}
//上面这两种情况,都是 any 类型
元组
众所周知,数组一般由同种类型的值组成,但有时我们需要在单个变量中存储不同类型的值,这时候我们就可以使用元组。在 JavaScript 中是没有元组的,元组是 TypeScript 中特有的类型,其工作方式类似于数组。
元组最重要的特性是可以限制 数组元素的个数和类型 ,它特别适合用来实现多值返回。****
元祖用于保存定长定数据类型的数据
let x: [string, number];
// 类型必须匹配且个数必须为2
x = ['hello', 10]; // OK
x = ['hello', 10,10]; // Error
x = [10, 'hello']; // Error
当我们确切的知道包含多少个元素,以及特定索引对应的类型,建议使用元组。
元组的解构赋值
let employee: [number, string] = [1, "芜湖"];
let [id, username] = employee;
console.log(`id: ${id}`);//id:1
console.log(`username: ${username}`);//username:芜湖
元组中的可选元素
let option:[string,boolean?]
元组类型的剩余参数
type RestTupleType = [number, ...string[]];
let restTuple: RestTupleType = [666, "Semlinker", "Kakuqo", "Lolo"];
console.log(restTuple[0]);
console.log(restTuple[1]);
只读的元组
const point: readonly [number, number] = [10, 20];
类型推断
在 TS 中,某些没有明确指出类型的地方,TS 的类型推论机制会帮助提供类型。
换句话说:由于类型推论的存在,这些地方,类型注解可以省略不写!
发生类型推论的2种常见场景:1.声明变量并初始化时 2.决定函数返回值时。
let age = 18
function add(num1:number,num2:number){
return num1+num2
}
注意:这两种情况下,类型注解可以省略不写。
推荐:能省略类型注解的地方就省略
类型断言
有时候你会比 TS 更加明确一个值的类型,此时,可以使用类型断言来制定更具体的类型。
// 尖括号 语法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
// as 语法
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
下面是一个实际的例子:
<a href="http://www.baidu.com" id="link">百度</a>
const alink = document.getElementById('link')
注意:getElementBYId 方法返回值的类型是 HTMLElement ,该类型只包括所有标签公共属性或方法,不包含 a 标签特有的 herf 等属性。
因此,这个类型太宽泛(不具体),无法操作 herf 等 a 标签特有的属性或方法。
解决方式:这种情况下就需要使用类型断言指定更加具体的类型。
使用类型断言:
const alink = document.getElementById('link') as HTMLAnchorElement
解释:
- 使用
as关键字实现类型断言。 - 关键字 as 后面的类型是一个更加具体的类型(TMLAnchorElement 是 HTMLElement 的子类型 )。
- 通过类型断言,alink 的类型变得更加的具体,这样就可以访问 a 标签特有的属性和方法了。
另一种语法,使用 <> 语法,这种语法形式不常用知道即可。
const alink = <HTMLAnchorElement>document.getElementById('link')
技巧:在浏览器控制台,通过 console.dir() 打印 DOM 元素,在属性列表的最后面,即可看到该元素的类型。
高级类型
class
基本使用
class Person {
age:number
// gender:string = '男'
gender = '男'
}
const p = new Person()
声明成员 age ,类型为 number(没有初始值)
声明成员 gender,并设置初始值,此时,可以省略注解
class Point {
x = 10
y = 10
scale(n:number):void{
this.x*=n
this.y*=n
}
}
方法的类型注解(参数和返回值)与函数用法相同
构造函数
class Person {
age:number
gender:string
constructor(age:number,gender:string){
this.age = age
this.gender = gender
}
}
let p = new Person(18,'男')
成员初始化(比如,age:number)后,才可以通过 this.age 来访问实例成员
需要为构造函数指定类型注解,否则会被隐私推倒为 any ;构造函数不需要返回值类型。
继承
类继承的两种方式:1.extends(继承父类) 2. implements(实现接口)
说明:JS 中只有 extends ,而 implement 是 TS 提供的。
class Animal{
move(){ concole,log('Moving along!') }
}
class Dog extends Animal{
bark(){ console.log('汪!') }
}
const dog = new Dog()
通过 extends 关键字实现继承
子类 Dog 继承父类 Animal,则 Dog 的实例对象 dog 就同时具有了父类 Animal 和子类 Dog 的所有属性和方法。
interface Singable {
sing():void
}
class Person implements Singable{
sing(){
console.log('啦啦啦啦啦啦')
}
}
通过 implements 关键字让 class 实现接口
Person 类实现接口 Singable 意味着,Person 类中必须提供 Singable 接口中指定的所有方法和属性。
可见性修饰符
在 TS 中,可见性修饰符包括:1.public(公有性) 2. protected(受保护的) 3. private(私有的)
-
public:表示公有的,公开的,公有成员可以被任何地方访问,默认可见性。
class Animal { public move(){ console.log('Moving along!') } }在类属性或方法面前添加 public 关键字,来修饰改属性或方法是公有的。
因为 public 是默认可见性,所以,可以直接省略。
-
protected:表示受保护的,仅对其声明所在类和子类中(非实例对象)可见。
class Animal { protected move() { console.log('Moving along!') } } class Dog extends Animal { bark(){ console.log('汪!') this.move() } }在类属性或方法前面添加 protected 关键字,来修饰该属性或方法是所保护的
在子类的方法内部可以通过 this 来访问父类中受保护的成员,但是,对实例不可见
-
private:表示私有的,只在当前类中可见,对实例对象以及子类也是不可见的
class Animal { private move(){ console.log('Moving along!') } walk(){ this.move() } }在类属性或方法前面添加 private 关键字,来修饰属性或方法是私有的
私有的属性或方法只在当前类中可见,对子类和实例对象也都是不可见的
-
readonly:表示==只读,用来防止在构造函数之外对属性进行赋值。
class Person { readonly age:number = 18 constructor(age:number){ this.age = age } }使用 readonly 关键字修饰该属性是只读的,注意只能修饰属性不能修饰方法
注意:属性 age 后面的类型注解(比如,此处的 number) 如果不加,则 age 的类型为 18(字面量类型)。
接口或者 {} 表示对象类型,也可以使用 readonly
interface IPerson { readonly name:string } let obj:IPerson = { name:'jack' } obj.name = 'rose'//报错,不能修改被readonly修改后的值
类型兼容性
对象之间的兼容性
TS 采用的是结构化类型系统,类型检查关注的是值所具有的形状。
也就是说,在结构类型系统中,如果两个对象具有相同的形状,则认为它们属于同一类型。
class Point { x:number;y:number }
class Point2D { x:number;y:number }
const p:Point = new Point2D()
解释:
- Point 和 Point2D 是两个名称不同的类
- 变量 p 的类型被显示标注为 Point 类型,但是,它的值却是 Point2D 的实例,并没有类型错误。
- 因为 TS 是结构化类型系统,只检查 Point 和 Point2D 的结构是否相同(相同,都具有 x 和 y 两个属性,属性类型也相同)
对于对象类型来说,y 的成员至少与 x 相同,则 x 兼容 y (成员多的可以赋值给成员少的)
class Point { x:number;y:number }
class Point3D { x:number;y:number;z:number }
const p:Point = new Point3D()
解释:
- Point3D 的成员至少与 Point 相同,则 Point 兼容 Point3D。
- 所以,成员多的 Point3D 可以赋值给成员少的 Point。
接口之间的兼容性
接口之间的兼容性,类似于 class 。并且,class 和 interface 之间也可以兼容。
interface Point { x:number;y:number }
interface Point2D { x:number;y:number }
interface Point3D { x:number;y:number;z:number }
let p1: Point = {x:0,y:1}
let p2: Point2D = {x:1,y:2}
let p3: Point3D = {x:2,y:3,z:4}
p1 = p2
p2 = p1
p2 = p3
class Point3D { x:number;y:number;z:number }
let p3:Point2D = new Point3D()
函数之间的兼容性
函数之间兼容性比较复杂,需要考虑:1.参数个数 2.参数类型 3.返回值类型
- 参数个数,参数多的兼容参数少的(或者说,参数少的可以赋值给多的)
type F1 = (a:number) => void
type F2 = (a:number,b:number) => void
let f1:F1 = (a:number):void =>{
console.log('F1')
}
let f2:F2 =(a:number,b:number):void =>{
console.log('F2')
}
f2 =f1
const arr = ['a','b','c']
arr.forEach(()=>>{})
arr.forEach((item)=>>{})
解释:
- 参数少的可以赋值给参数多的,所以,f1 可以赋值给 f2
- 数组 forEach 方法的第一个参数是回调函数,该示例中类型为: (value:string,index:number,array:string[])=>{}
- 在 JS 中省略用不到的函数参数实际上是非常常见的,这样的使用方式,促成了 TS 中函数类型之间的兼容性。
- 参数类型,相同位置的参数类型要相同 (原始类型) 或 兼容 (对象类型)
type F1 = (a:number) => void
type F2 = (a:number) => void
let f1:F1 = (a:number):void =>{
console.log('F1')
}
let f2:F2 =(a:number):void =>{
console.log('F2')
}
f2 =f1
解释:函数类型 F2 兼容函数类型 F1,因为 F1 和 F2 的第一个参数类型相同。
如果参数是一个对象,将对象拆开,把每个属性看做一个个参数,参数少的可以赋值给参数多的。
- 返回值类型
type F1 = (a:number) => void
type F2 = (a:number) => void
let f1:F1 = (a:number):void =>{
console.log('F1')
}
let f2:F2 =(a:number):void =>{
console.log('F2')
}
f2 =f1
如果返回值类型是原始类型,此时两个类型要相同,比如,上面两个(返回值都是void)
type F1 = () => { name:string }
type F2 = () => { name:string;age:number }
如果返回值类型是对象类型,此时成员多的可以赋值给成员少的,比如上面的例子,可以将F2的函数 赋值给 F1的函数
交叉类型(&)
交叉类型的功能类似于接口继承(extends),用于组合多个类型为一个类型(常用于对象类型)。
interface Person { name:string }
interface Contact { phone:string }
type PersonDetail = Person & Contact
let obj:PersonDetail = {
name:"jack",
phone:"1264"
}
解释:使用交叉类型后,新的类型 PersonDetail 就同时具备了 Person 和 Contact 的所有属性类型。
相当于:
type PersonDetail = { name:string;phone:string }
交叉类型(&) 和 接口继承(extends) 的对比:
- 相同点:都可以实现对象类型的组合
- 不同点:两种方式实现类型组合时,对于同名属性之间,处理类型冲突的方式不同。
interface A{
fn:(value:number) => string
}
interface B extends A{
fn:(value:string) => string
}
此时这里的接口继承会报错,类型不兼容。
interface A {
fn:(value:number) => string
}
interface B {
fn:(value:string) => string
}
type C = A & B
此时就不会报错,相当于:
fn:(value:string|number) => string
泛型
基本使用
泛型是可以在保证类型安全前提下,让函数等与多种类型一起工作,从而实现复用,常用于:函数,接口,class中。
前提需求
创建一个 id 函数,传入什么数据就返回改数据本身(也就是说,参数和返回值类型相同)。
function id(value:number):number { return value }
比如,id(10) 调用以上函数就会直接返回 10 本身。但是,该函数值接收数值类型,无法用于其他类型。
为了能让函数能够接受任意类型,可以将参数类型修改为 any 。但是,这样就失去了 TS 的类型保护,类型不安全。
function id(value:any):any { return value }
泛型在保证类型安全(不丢失类型信息)的同时,可以让函数等于多种不同的类型一起工作,灵活可复用。
创建泛型函数:
function id<Type>(value:Type):Type { return value }
解释:
- 语法:在函数名称的后面添加
<>(尖括号)中添加类型变量,比如此处的Type. - 类型变量 Type ,是一种特殊类的变量,它处理的类型不是值。
- 该类型变量相当于一个类型容器,能够捕获用户提供的类型(具体是什么类型由用户调用该函数的时指定)。
- 因为 Type 是类型,因此可以将其作为参数和返回值的类型,表示参数和返回值具有相同的类型。
- 类型变量 Type ,可以是雷伊合法类型的变量名称。
调用泛型函数
const num = id<number>(10)
const str = id<string>('a')
解释:
- 语法:在函数名后面添加
<>(尖括号) ,尖括号中指定具体的类型,比如,此处的number - 当传入类型 number 后,这个类型就会被函数声明时指定的类型变量 Type 捕获到。
- 此时,Type 的类型就是 number ,所以,函数 id 参数和返回值的类型也都是 number。
同样,如果传入的类型 string ,函数 id 参数和返回值的类型都是 string
这样,通过泛型就做到了让id函数与多种不同类型一起工作,实现了复用的同时保证了类型安全。
简化调用泛型函数
let num = id<number>(10)
上面的函数调用其实可以简化为下面的形式:
let num = id(10)
解释:
- 在调用泛型函数时,可以省略<类型> 来简化泛型函数的调用。
- 此时,TS 内部会采用一种叫做类型参数推断的机制,来根据传入的实参自动推断出变量
Type的类型。 - 比如,传入实参 10 ,TS 会自动的推断出变量 num 的类型 number ,并作为 Type 的类型。
虽然使用这种简化地方方式调用泛型函数,使代码更短,更易于阅读。但是,当编译器无法判断类型或者推断的类型不准确时,就需要显示地传入类型参数。
泛型约束
默认情况下,泛型函数的类型类型变量 Type 可以代表多个类型,这导致无法访问任何属性。
function id<Type>(value:Type):Type{
console.log(value.length)
return value
}
比如说,上面这个例子,是错误的。
Type 可以代表任意类型,无法保证一定存在 length 属性,比如 number 类型就没有 length。
此时,就需要为泛型添加约束来收缩类型(缩窄类型取值范围)。
interface ILength { length:number }
function id<Type extends ILength>(value:Type):Type{
console.log(value.length)
return value
}
解释:
- 创建描述约束的接口 ILength,该接口要求提供 ILength 属性。
- 通过 extends 关键字使用该接口,为泛型(类型变量)添加约束。
- 该约束表示:传入的类型必须有 length 属性。
注意:传入的实参 (比如,数组) 只要有 length 属性即可,这也符合前面讲到的接口类型兼容原则。
泛型的类型变量可以有多个,并且类型变量之间还可以约束(比如,第二个类型变量受第一个类型变量约束)
比如,创建一个函数来获取对象中属性的值:
function getProp<Type,Key extends keyof Type>(obj:Type,key:Key){
return obj[key]
}
let person = { name:'jack',age:18 }
let a = getProp(person,'name')
console.log(a)
解释:
- 添加了第二个类型变量 Key,两个类型变量之间使用
,逗号分隔。 keyof关键字 接受一个对象类型,生成其键名称 (可能是字符串或数字) 的联合类型。- 本实例中
keyof Type实际上获取的是person对象所有键的联合类型,也就是:'name'|'age'。 - 类型变量 Key 受 Type 约束,可以理解为:Key 只能是 Type 所有键中的任意一个,或者说只能访问对象中存在的属性。
泛型接口
接口也可以配合泛型来使用,以增加其灵活性,增强其复用性。
interface IdFunc<Type>{
id:(value:Type) => Type
ids:()=>Type[]
}
let obj:IdFunc<number> = {
id(value) { return value },
ids(){ return [1,2,3,4] }
}
解释:
- 在接口名称后面添加 <类型变量> ,那么,这个接口就变成了泛型接口。
- 接口的类型变量,对接口中使用其他成员可见,也就是接口中所有成员都可以使用类型变量。
- 使用泛型接口时,需要显示指定具体的类型(比如,此处的IdFunc)
- 此时,id 方法的参数和返回值都是 number,ids 方法的返回值类型是 number[]
泛型类
创建泛型类
class GenericNumber<NumType>{
defaultValue: NumType
add:(x:NumType,y:NumType)=>NumType
}
类似于泛型接口,在 class 名称后面添加 <类型变量> ,这个类就变成了泛型类。
使用泛型类
const myNum = new GenericNumber<number>()
myNum.defaultValue = 10
类似于泛型接口,在创建 class 实例时,在类名后面通过 <类型> 来指定明确的类型。
泛型工具类型
TS 内置了一些常用的工具类型,来简化 TS 中的一些常见操作。
说明:它们都是基于泛型实现的(泛型适用于多种类型,更加通用),并且是内置的,可以直接在代码中使用。
Partial
用来构造(创建)一个类型,将 Type 的所有属性设置为可选。
interface Props{
id:string
children:number[]
}
type PatialProps = Partial<Props>
// 这里面的两个参数必须写,不写会报错
let p1:Props = {
id:'',
children:[1]
}
// 这里面的参数可写可不写
let p2:PatialProps = {}
构造出来的新类型 PatialProps 结构 和 Props 相同,但所有属性都变为可选的。
Readonly
用来构造一个类型,将 Type 的所有属性都设置为 readonly(只读)
interface Props {
id:string
children:number[]
}
type ReadonlyProps = Readonly<Props>
构造出来的新类型 ReadonlyProps 结构和 Props 相同,但所有属性都变为只读的。
let props:ReadonlyProps = { id:'1',children:[] }
// 报错,不能修改
// props.id = '2'
当我们想要重新给 id 属性赋值时,就会报错:无法分配到“id”,因为它是只读属性。
Pick<Type,Keys>
从 Type 中选择一组属性来构造新类型。
interface Props{
id:string
title:string
children:number[]
}
type PickProps = Pick<Props,'id'|'title'>
const a:PickProps = {
id:'111',
title:'222'
}
解释:
- Pick 工具类型由两个类型变量:1.表示选择谁的属性2.表示选择哪几个属性。
- 其中第二个类型变量,如果只选择一个则只传入该属性名即可。
- 第二个类型变量传入的属性只能是第一个类型变量中存在的属性。
- 构造出来的新类型 PickProps,只有 id 和 title 两个属性类型。
Record<Keys,Type>
构造一个对象类型,属性为 Keys ,属性类型为 Type。
type RecordObj = Record<'a'|'b'|'c',string[]>
let obj:RecordObj = {
a:['1'],
b:['2'],
c:['3']
}
解释:
- Record 工具类型由连个类型变量:1. 表示对象有哪些属性2. 表示对象属性的类型。
- 构建的新对象类型 RecordObj 表示:这个对象有三个属性,属性值的类型都是 string[]
// 如果不像上述的那样,就需要
type RecordObj = {
a:string[],
b:string[],
c:string[]
}
更多的泛型类工具请参考:2021 typescript史上最强学习入门文章(2w字) - 掘金 (juejin.cn)
索引类型签名
绝大多数情况下,我们都可以在使用对象之前就确定对象的结构,并为对象添加准确的类型。
使用场景:当无法确定对象中有哪些属性(或者说对象中可以出现任意多个属性),此时,就要用到索引签名类型了。
interface AnyObject {
[key:string]:number
}
let obj:AnyObject = {
a:1,
b:2
}
解释:
- 使用
[key:string]来约束该接口中允许出现的属性名称,表示只要是 string 类型的属性名称,都可以出现在对象中 - 对象 obj 中就可以出现任意多个属性(比如,a,b等)。
- key只是一个占位符,可以换成任意合法的变量名称。
- 隐藏的前置知识:js 中对象 ({ }) 的键是 string 类型的。
映射类型
基于旧类型创建新类型(对象类型),减少重复,提高开发效率。
比如,类型 PropKeys 有 x/y/z,另一个类型 Type1 中也有 x/y/z,并且 Type1 中 x/y/z 的类型相同:
type PropKeys = 'x'|'y'|'z'
type Type1 = { x:number;y:number;z:number }
这样书写,没错,但 x/y/z 重复书写了两次。想这样的情况,就可以使用映射类型来进行简化。
type PropKeys = 'x'|'y'|'z'
type Type2 = { [key in PropKeys]:number }
解释:
- 映射类型是基于索引签名类型的,所以,该语法类似于索引q签名类型,也使用了
[] key in PropKeys表示 key 可以是 Propkeys 联合类型中的任意一个,类似于forin(let k in obj)- 使用映射类型创建的新对象类型 Type2 和类型 Type1 结构完全相同。
- 注意:映射类型只能在类型别名中使用,不能在接口中使用。
keyof
映射类型处理根据联合类型创建新类型外,还可以根据对象类型来创建:
type Props = { a:number;b:string;c:boolean }
type Type3 = { [key in keyof Props]:number }
解释:
- 首先,先执行
keyof Props获取到对象类型 Props 中所有键的联合类型,即'a'|'b'|'c' - 然后,
key in ...就表示 key 可以是 Props 中所有的键名称中的任意一个。
泛型工作类型原理(以partial为例)
type Partial<T> = {
[P in keyof T]?:T[P]
}
interface Props{
id:string
children:number[]
}
type PatialProps = Partial<Props>
索引查询访问类型
上面用到的 T[P] 语法,在 TS 中,叫做 索引查询(访问类型) 。
作用:用来查询属性的类型。
type Props = { a:number;b:string;c:boolean }
type TypeA = Props['a']
Props['a'] 表示查询类型 Props中属性 ‘a’ 对应的类型 number。所以,TypeA 的类型为 number。
注意:[] 中的属性必须存在于被查询类型中,否则会报错。
同时查询多个索引类型
type Props = { a:number;b:string;c:boolean }
type TypeA = props['a'|'b']//string|number
type TypeA = Props[keyof Props]//string|number|boolean
类型声明文件
TS中的两种文件类型
TS 中有两种文件类型:1.ts 文件 2.d.ts文件
-
ts文件
- 既包含类型信息又可执行代码。
- 可以被编译为.js文件,然后,执行代码。
- 用途:编写程序代码的地方。
-
.d.ts文件
- 只包含信息类信息的类型声明文件。
- 不会生成.js文件,仅用于提供类型信息。
- 用途:为 js 提供类型信息。
// .d.ts
// 类型
type Props1 = { x:number;y:number }
// 可执行代码(会报错!!!)
// function add(num1:number,num2:number):number{
// return num1+mnum2
// }
.ts是代码实现文件;.d.ts是类型声明文件如果要为 JS 库提供类型信息,要使用 .d.ts 文件。
使用第三方库的类型声明文件
使用已有的类型声明文件有两种:1. 内置类型声明文件 2. 第三方库的类型声明文件。
第三方的库文件有两种存在形式:1.库自带类型声明文件 2.由 DefinitelyTyped 提供。
-
库自带类型声明文件:比如,axios
由于库自带类型声明文件,这种情况下,正常导入该库,TS 就会自动加载库自己的类型声明文件,以提供该库的类型声明。
-
由 DefinitelyTyped 提供。
DefinitelyTyped 是一个 github 仓库,用来提供高质量的 Typescript 类型声明。
可以通过 npm/yarn 来下载仓库提供提供的 TS 类型声明包。
创建出自己的类型声明文件
项目内共享类型
如果多个 .ts 文件中都用到同一个类型,此时可以创建 .d.ts 文件提供该类型,实现类型共享。
操作步骤:
- 创建
index.d.ts类型声明文件。 - 创建需要共享的类型,并使用 export 导出。
- 在需要使用共享类型的
.ts文件中,通过 import 导入即可。
为已有的 JS 文件提供声明
说明:TS 项目中也可以使用. js 文件。
说明:在导入 .js 文件时,TS 会自动加载与 .js 同名的 .d.ts 文件,以提供类型声明。
declare 关键字:用于类型声明,为其他地方(比如,.js文件)已存在的变量声明类型,而不是创建一个新的变量。
declare let count:number
向上面这样,就为变量提供了一个类型声明,函数等也是如此。
tsconfig.json
重要字段
- files - 设置要编译的文件的名称;
- include - 设置需要进行编译的文件,支持路径模式匹配;
- exclude - 设置无需进行编译的文件,支持路径模式匹配;
- compilerOptions - 设置与编译流程相关的选项。
compilerOptions 选项
{
"compilerOptions": {
/* 基本选项 */
"target": "es5", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
"module": "commonjs", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
"lib": [], // 指定要包含在编译中的库文件
"allowJs": true, // 允许编译 javascript 文件
"checkJs": true, // 报告 javascript 文件中的错误
"jsx": "preserve", // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
"declaration": true, // 生成相应的 '.d.ts' 文件
"sourceMap": true, // 生成相应的 '.map' 文件
"outFile": "./", // 将输出文件合并为一个文件
"outDir": "./", // 指定输出目录
"rootDir": "./", // 用来控制输出目录结构 --outDir.
"removeComments": true, // 删除编译后的所有的注释
"noEmit": true, // 不生成输出文件
"importHelpers": true, // 从 tslib 导入辅助工具函数
"isolatedModules": true, // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似).
/* 严格的类型检查选项 */
"strict": true, // 启用所有严格类型检查选项
"noImplicitAny": true, // 在表达式和声明上有隐含的 any类型时报错
"strictNullChecks": true, // 启用严格的 null 检查
"noImplicitThis": true, // 当 this 表达式值为 any 类型的时候,生成一个错误
"alwaysStrict": true, // 以严格模式检查每个模块,并在每个文件里加入 'use strict'
/* 额外的检查 */
"noUnusedLocals": true, // 有未使用的变量时,抛出错误
"noUnusedParameters": true, // 有未使用的参数时,抛出错误
"noImplicitReturns": true, // 并不是所有函数里的代码都有返回值时,抛出错误
"noFallthroughCasesInSwitch": true, // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)
/* 模块解析选项 */
"moduleResolution": "node", // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
"baseUrl": "./", // 用于解析非相对模块名称的基目录
"paths": {}, // 模块名到基于 baseUrl 的路径映射的列表
"rootDirs": [], // 根文件夹列表,其组合内容表示项目运行时的结构内容
"typeRoots": [], // 包含类型声明的文件列表
"types": [], // 需要包含的类型声明文件名列表
"allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。
/* Source Map Options */
"sourceRoot": "./", // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
"mapRoot": "./", // 指定调试器应该找到映射文件而不是生成文件的位置
"inlineSourceMap": true, // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
"inlineSources": true, // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性
/* 其他选项 */
"experimentalDecorators": true, // 启用装饰器
"emitDecoratorMetadata": true // 为装饰器提供元数据的支持
}
}
参考文献
2021 typescript史上最强学习入门文章(2w字) - 掘金 (juejin.cn)
黑马程序员前端TypeScript教程,TypeScript零基础入门到实战全套教程_哔哩哔哩_bilibili
结语
文章如果有不正确的地方,欢迎指正,共同学习,共同进步。
若有侵权,请联系作者删除。