一、介绍
TypeScript是JS的超集,为JS添加了类型支持。
1.1 为什么添加类型支持
JS代码的错误大部分是类型错误,增加改Bug时间,影响开发效率。
静态类型:编译期做类型检查
动态类型:执行期做类型检查
TS--静态类型编程语言,代码执行时发现错误
JS--动态类型编程语言,代码编译时发现错误
1.2 TS的优势
- 减少改Bug时间,提升开发效率
- 类型系统提升代码的可维护性,重构代码更容易
- 支持ECMAScript语法
- 类型推断机制,自动根据代码逻辑推断数据类型
二、使用
2.1 安装使用
npm i -g typescript
tsc -v --验证是否安装成功
2.2 初体验
创建hello.ts文件
tsc hello.ts --将TS编译为JS
node hello.js --执行JS代码
2.3 简化运行步骤
每次修改代码都要编译成JS在执行,太麻烦
npm i -g ts-node
ts-node hello.ts --在内部隐式转换成JS,不生成JS文件
三、常用类型
3.1 类型注解
let age: number = 18
约定类型必须和赋值类型一致
3.2 常用基础类型
3.2.1 JS已有类型
基础类型 Undefined | Number | Bigint | String | Boolean | Symbol
引用类型 Object(Array) | Function
3.2.2 TS新增类型
联合类型、自定义类型、接口、元组、字面量、枚举、void、any
3.3 原始类型
number / string / boolean / null / undefined / symbol
这些类型按照JS中类型名称书写
let age: number = 18
let isLoading: boolean = false
3.4 数组类型
两种写法
let numbers: number[] = [1, 3, 5]
let strings: Array<string> = ['a', 'b', 'c']
如果一个数组有两个或以上类型,即联合类型
let mergeArr: (number | string)[] = [1,'a',3,'b']
//如果不添加括号 表示这个变量可以是number也可以是string数组
let x: number | string[] = ['a','b']
let y: number | string[] = 10
3.5 类型别名
当同一类型被多次使用时,类型别名可以简化书写
关键字type
type CustomArray = (number | string)[]
let arr1: CustomArray = [1, 'a', 3]
let arr2: CustomArray = ['x', 'y', 2]
3.6 函数类型
函数类型指的是参数类型和返回值类型
3.6.1 单独指定参数、返回值的类型
//函数声明
function add(n1: number, n2: number):number{
return n1 + n2
}
//函数表达式
const add = (n1: number, n2:number):number => {
return n1 + n2
}
3.6.2 同时指定参数、返回值的类型
const add: (n1: number, n2: number) => number = (n1, n2) => {
return n1 + n2
}
3.6.3 如果函数没有返回值,声明为void
function greet(name: string): void{
console.log('Hello', name)
}
3.6.4 可选参数
参数名后面加问号,可选参数只能出现在列表最后,也就是可选参数后面不能再出现必选参数
function introduction(name: string, age?: number): void{
console.log(`我叫${name}`)
if(age){
console.log(`我今年${age}岁`)
}
}
3.7 对象类型
- 直接使用{}来描述对象结构,属性采用属性名:类型的形式;方法采用方法名():返回值类型的形式
- 如果方法有参数,就在方法名后面的小括号指定参数类型greet(name:string):void
- 在一行代码中指定对象的多个属性类型时,使用;分隔
let person: {
name: string,
age: number,
// sayHi():void
sayHi: () => void
} = {
name:'Augustine',
age:18,
sayHi(){
console.log(`hi! ${this.name}`)
}
}
可选属性
给可选属性名后加问号,与函数可选参数类似
3.8 接口
3.8.1 用法
当一个对象类型被多次使用,一般使用接口来表述对象便于复用
interface IPerson {
name: string,
age: number,
sayHi: () => void
}
let person1: IPerson = {
name:'Mary',
age:19,
sayHi() {
console.log(`${this.name}, Hi!`)
},
}
type和interface
interface只能为对象指定类型
type可以为任意类型指定别名
3.8.2 继承
如果两个接口有相同的属性或方法,可以将公共属性或方法抽离出来,通过继承实现复用
interface Point2D {x: number; y: number}
interface Point3D {x: number; y: number; z: number}
//可以写为
interface Point2D {x: number; y: number}
interface Point3D extends Point2D {z: number}
3.9 元组
使用number[]的缺点是无法确切知道数组长度
元组(tuple)确切知道包含多少个元素,以及特定索引对应的类型,少一个多一个都不行
let position [number, number] = [1, 2]
3.10 类型推论
TS中没有明确指出类型的地方,类型推论机制可以帮助提供类型
两种情况:
声明变量并初始化值
决定函数返回值
类型注解能省则省,提高开发效率
3.11 类型断言
指定更加具体的类型
当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型中共有的属性或方法
getElementById返回值的类型是HTMLElement,只包含所有标签的公共属性和方法
比如<a>可以拿到.id属性 但不能识别.href
通过as实现类型断言 后面跟一个更具体的类型
const aLink = document.getElementById('link') as HTMLAnchorElement
3.12 字面量类型
let str1 = 'hello' //类型为string
const str2 = 'hello' //类型为hello
常量的值不能变化,这里的“hello”就是一个字面量类型,即某个特定的字符串也可以成为TS中的类型,除此之外任意的JS字面量都可以作为类型使用
使用场景:表示一组明确的可选值
比如在贪吃蛇游戏里的方向只能是up、down、left、right中的一种
function selectDirection(direction: 'up' | 'down' | 'left' | 'right'){
console.log(direction)
}
3.13 枚举类型
描述一个值,并定义一组命名常量,这个值可以是这组常量里任何一个。
- enum关键字定义枚举
- 枚举名称大写字母开头
- 枚举的多个值之间通过逗号分隔
- 使用这个枚举用枚举名称做类型注解
enum Direction{Up, Down, Left, Right}
function selectDirection(direction: Direction){
console.log(direction)
}
//.访问枚举成员
selectDirection(Direction.Up)
枚举成员的默认值是从0开始自增的数字(数字枚举),也可以自行给枚举成员初始化值
字符串枚举:枚举成员的值是字符串,每个成员必须有初始值
enum Direction{
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT'
}
//编译后的js代码
//生成一个立即执行函数并传入Direction(作为对象)
var Direction;
(function (Direction) {
Direction["Up"] = "UP";
Direction["Down"] = "DOWN";
Direction["Left"] = "LEFT";
Direction["Right"] = "RIGHT";
})(Direction || (Direction = {}));
一般推荐字面量+联合类型组合方式,因为枚举会被编译成函数,带来的开销更大。
3.14 any类型
不推荐使用,因为会让TS失去类型保护的优势
当一个值的类型为any,可以对该值进行任意操作,并且不会有代码提示。
隐式any情况:(不建议这样做)
- 声明变量不提供类型和初始值
- 函数参数不加类型
3.15 typeof
第一种用法与JS相同 获取数据类型
第二种用法:在类型注解中引用变量或属性的类型(简化类型书写),不能查询函数调用的类型
let p :{ x: number, y: number}
function formatPoint(point: typeof p){
console.log(point)
}
formatPoint({x:2, y:5})
四、高级类型
4.1 class类
TS支持ES6中class关键字,用class创建的类也作为一种类型存在
4.1.1 基本使用
class Person{
name: string
age: number
gender = 'male'
//有初始值的属性可以省略类型注解
constructor(name: string, age: number, gender?: 'male' | 'female'){
//构造函数里不能指定返回值类型注解
this.name = name
this.age = age
if(gender){
this.gender = gender
}
}
}
const bob = new Person('bob', 18)
console.log(bob)
4.1.2 实例方法
class Circle{
x: number
y: number
radius: number
constructor(x: number, y: number, radius: number){
this.x = x
this.y = y
this.radius = radius
}
//设置计算属性
get area() {
return Math.PI * this.radius ** 2;
}
//方法的类型注解与普通函数相同
scale(n: number):void{
this.radius *= n
}
}
const c = new Circle(1, 2, 3)
console.log(c.area)//28.274333882308138
c.scale(3)
console.log(c.area)//254.46900494077323
4.1.3 类继承
extends (JS自带)
子类继承父类,子类的实例对象可以访问父类的属性和方法
class Animal{
eat(){
console.log('eat')
}
}
class Dog extends Animal{
bark(){
console.log('woof')
}
}
const d = new Dog()
d.eat()
d.bark()
implements (TS特有)
强制类遵循特定的接口定义。当一个类实现了一个接口时,它必须实现接口中定义的所有属性和方法。
interface Shape {
calculateArea(): number;
}
class Circle implements Shape {
radius: number;
constructor(radius: number) {
this.radius = radius;
}
calculateArea(): number {
return Math.PI * this.radius ** 2;
}
}
let circle = new Circle(5);
console.log(circle.calculateArea()); // 输出圆的面积,约为 78.54
4.1.4 类成员可见性
可见性修饰符
public - 公有的,被修饰的属性或方法可以被任何地方访问(默认)
protected - 受保护的,仅对其声明所在类和子类中可见(在子类的方法内部可以通过this访问,实例对象不可见)
private - 私有的,只在当前类中可见,实例对象以及子类不可见
只读修饰符
readonly - 表示只读,用来防止在构造函数之外对属性进行赋值,不能修饰方法
- 使用readonly指定属性值一定要声明属性值的类型
- 可以用来声明接口的属性和对象属性
4.2 类型兼容性
两种类型系统
结构化类型系统(Structural Type System):按照属性和方法是否相同来比较两个类是否相同
标明类型系统(Nominal Type System):只看两个类的名称是否相同,即便内部结构完全相同也不能认为是同一种类型
TS采用结构化类型系统,而像C++、java采用标明类型系统。
4.2.1 类与接口的类型兼容性
class Point{
x: number
y: number
}
class Point2D{
x: number
y: number
}
//产生类型兼容性 不会报错
const p: Point = new Point2D()
更准确的说法:对于对象类型,如果A的成员是B的子集,则B可以赋值给A
class Point{
x: number
y: number
}
class Point3D{
x: number
y: number
z: number
}
//产生类型兼容性 不会报错
const p: Point = new Point3D()
接口之间、类与接口之间的兼容性与类之间的相同。
4.2.2 函数的类型兼容性
考虑:参数个数、参数类型、返回值类型
参数个数-参数少的可以赋值给参数多的
参数类型-相同位置的参数类型要相同或兼容
interface Point2D{
x: number
y: number
}
interface Point3D{
x: number
y: number
z: number
}
type F2 = (p: Point2D) => void
type F3 = (p: Point3D) => void
let f2: F2
let f3: F3
f3 = f2
f2 = f3 //不兼容
返回值类型-如果是基本类型,两个类型要相同;如果是对象类型,成员多的可以赋值给成员少的
4.3 交叉类型
&表示,组合多个类型为一个类型(常用于对象)
interface Person{
name: string
}
interface Contact{
phone: string
}
type PersonDetail = Person & Contact
let obj: PersonDetail = {
name: 'alice',
phone:'123456'
}
交叉类型与接口继承的不同:
接口继承中同名属性的类型不同会报错,而交叉类型对于同名属性类型会进行重载
interface A{
fn: (value: number) => string
}
interface B{
fn: (value: string) => string
}
type C = A & B
//则C中fn为
//fn: (value: string | number) => string
4.4 泛型
4.4.1 泛型函数与泛型接口
在保证类型安全的前提下(不使用any)让函数支持多种类型的调用 ,从而实现复用,常用于:函数、接口、class
函数名称后加<>,它可以捕获调用函数时提供的数据类型,可以将其作为函数参数和返回值的类型
function id<Type>(value: Type):Type{
return value
}
//在<>中指定具体的类型 就会被函数声明时指定的类型变量Type捕获到
const num = id<number>(10)
const str = id<string>('a')
数组在TS中就是一个泛型接口,使用数组时TS会根据数组的不同类型,来自动将类型变量设置为相应的类型
const str = ['a', 'b', 'c']
str.forEach(item => {
//item 类型为 string
})
const nums = [1, 2, 3]
nums.forEach(item => {
//item 类型为number
})
const mixArr = [1, 'a', 'w']
mixArr.forEach(item => {
//item 类型为string | number
})
4.4.2 泛型类
class GenericClass<Type>{
defaultValue: Type
add: (x: Type, y: Type) => Type
}
const myNumClass = new GenericClass<number>()
myNumClass.defaultValue = 10
const myStrClass = new GenericClass<string>()
myStrClass.defaultValue = 'a'
4.4.3 泛型约束
因为事先不知道到底是哪种类型,所以直接访问泛型变量的属性或函数会报错,就像这样:
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); //类型T上不存在属性length
return arg;
}
所以使用接口对泛型进行约束,只允许函数传入符合这个接口规则的变量:
interface Lengthwise {
length: number;
}
//传入的变量必须包含length属性
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
4.4.4 泛型工具类型
Partial<Type>--构造一个类型,将Type的所有属性设置为可选
interface Props{
id: string
children: number[]
}
type ParticalProps = Partial<Props>
下图为ParticalProps的真实类型
Readonly<Type>--构造一个类型,将Type的所有属性设置只读
interface Props{
id: string
children: number[]
}
type ReadonlyProps = Readonly<Props>
Pick<Type, Keys> 从type中选择一组属性来构造新类型
- Type表示选择谁的属性,keys表示选择哪几个属性
- keys只能传入Type中包含的属性
interface Props{
id: string
title: string
children: number[]
}
type PickProps = Pick<Props, 'id' | 'title'>
Record<Keys, Type>--构造一个对象类型,属性键为keys,属性类型为type
type RecordObj = Record<'a' | 'b' | 'c', string[] | number>
//同义于
//type RecordObj = {
// a: string[]
// b: string[]
// c: string[]
// }
let obj: RecordObj = {
a: ['1'],
b: ['2'],
c: ['3']
}
4.5 索引签名类型
无法确定对象中有哪些属性时就需要索引签名
//表示key如果为string类型的属性名都可以出现在该对象中,并且属性值为number
interface AnyObject{
[key: string]: number
}
数组对应的泛型接口,也使用了索引签名,下面模拟了原生的数组接口
//只要是number类型的键都可以出现在数组中
interface MyArray<T>{
[n: number]: T
}
4.6 映射类型
4.6.1 基于联合类型创建映射
基于旧类型(联合类型)创建新类型,减少重复书写
type PropKeys = 'x' | 'y' | 'z'
type Type1 = { x: number; y: number; z:number}
//使用映射
type Type2 = {[key in PropKeys]: number}
- 映射类型基于索引签名类型
- key in PropKeys表示key必须和PropKeys联合类型中相同
- 使用映射类型创建的新对象类型Type2和Type1的结构完全相同
- 映射类型只能在类型别名中使用,不能在接口中使用
4.6.2 基于对象类型创建映射
也可以根据对象类型创建新类型
type Props = { a: number, b: string, c: boolean}
type Type3 = {[key in keyof Props]: number}
- keyof 是获取该对象所有键的联合类型即 'a' | 'b' | 'c'
- 然后 key in keyof Props表示key必须是Props和所有键名称相同
4.6.3 使用映射实现Partical<Type>
泛型工具类型都是基于映射类型实现的
//Partial<Type>实现 -- 让所有类型变为可选
type Partial<T> = {
[P in keyof T]?: T[P]
}
- []后面添?表示将所有属性设置为可选
- T[P]表示获取T中每个键对应的类型 (索引查询)
索引查询的用法
type Props = { a: number, b: string, c: boolean}
type TypeA = Props['a']//number
type TypeB = Props['a'|'b'] //number | string
type TypeC = Props[keyof Props] //number | string | boolean