1.TypeScript准备
type+javascript
TypeScript是JavaScript的超集:js有的ts都有
1.1 为什么要为JS添加类型支持
JS的系统存在先天缺陷,JS代码中绝大部分错误都是“Uncaught TypeError”。 问题:增加了找bug,改bug的时间,严重影响开发效率
代码先编译,后执行
TypeScript:静态类型语言,在编译期做类型检查。代码在编译(执行前)就发现错误。
JavaScript:动态型语言,在执行期做类型检查。代码真正执行的时候去发现错误
1.2 安装TypeScript
Node和浏览器不能直接运行TS,需要把TS转化成JS。
- 实现TypeScript向JavaScript的转变
npm i typescript -g #全局安装TypeScript
tsc -v #查看版本
tsc hello.ts #编译ts文件:生成js文件
node hello.js #执行js代码
- 简化运行TS的步骤
ts-node可以编译和执行ts文件
npm i -g ts-node #安装ts-node包
ts-node hello.ts #将TS转化成JS,然后运行JS代码
2.TS基础类型
2.1 原始类型
number、string、boolean、undefined、null、symbol 冒号后面的内容叫:类型注解
let username: string = "张老师";
let age: number = 21;
let isLoading: boolean = true
let a: null = null;
let b: undefined = undefined;
let c: symbol = Symbol();
2.2 数组类型
//数组类型
let numbers: number[] = [100, 2003, 2020]
let numbers1: Array<number> = [2020, 2021, 2022]
//联合类型
let arr: (number | string)[] = [2002, 20003, "北京", "上海"]
let arr1: number | string[] = 2002
2.3 类型别名
相同的类型反复使用,这时候
type typeArray = (number | string)[]
let arr1: typeArray = [2002, 2003]
let arr2: typeArray = ["北京", "上海"]
2.4 函数类型
给函数添加类型,实际给参数和返回值添加类型
//普通函数
function fn(num1: number, num2: number): number {
return num1 + num2
}
//箭头函数
const fn1=(num1: number, num2: number): number => {
return num1 + num2
}
//同时指定参数和返回值类型
const fn3: (num1: number, num2: number) => number = (num1, num2) => {
// return 100
return num1 + num2
}
console.log(fn3(100, 200))
//void类型:没有返回值类型
const fn4 = function (username: string): void {
console.log("hello" + username)
}
//可选参数
function mySlice(start?: number, end?: number) {
console.log('起始索引', start, '结束索引', end)
}
mySlice()
mySlice(10)
mySlice(10, 20)
2.5 对象类型
//写在同一行
let person: { name: string; age: number; say(): void } = { name: "张三", age: 21, say() { } }
//换行
let person1: {
name: string,
age: number,
say(): void
greet(name: string): void
} = {
name: "张三",
age: 21,
say() {
console.log("北京欢迎你")
},
greet() {
console.log("我在打招呼")
}
}
- 可选类型
//对象的可选类型
function myAxios(config: { url: string, method?: string }) {
}
myAxios({
url: "http://www.baidu.com"
})
- 接口类型
当一个对象类型被多次使用时,一般会使用接口(interface)来描述对象的类型,达到复用的目的。
//接口类型
interface IPerson {
name: string,
age: number,
say(): void
}
let person: IPerson = {
name: "张三",
age: 19,
say() {
console.log("我正在说话")
}
}
- 类型别名和接口的区别:
- 接口,只能为对象指定类型
- 类型别名,不仅可以为对象指定类型,实际上可以为任意类型指定别名。
//接口类型
type IPerson = {
name: string,
age: number,
say(): void
}
//类型别名
let person: IPerson = {
name: "张三",
age: 19,
say() {
console.log("我正在说话")
}
}
- 接口的继承
新接口继承了旧接口中的所有属性和方法
//接口的继承
interface point2D {
x: number,
y: number
}
interface point3D extends point2D {
z: number
}
const p3: point3D = {
x: 100,
y: 100,
z: 100
}
2.6 元组
元组是另一种类型的数组,是明确元素数量和类型的数组
//元组
let position: [number, number] = [140, 42]
2.7 类型推断
能省略类型注解的地方则省略,尽量使用ts的类型推断,这样能提高开发效率
//age的类型自动为string
let age = 100;
age = "100" //ts会检测出错误
//函数的返回值没有指定类型,自己推论为number
function sum(num1: number, num2: number) {
return num1 + num2
}
2.8 字面量
使用模式:字面量类型配合联合类型一起使用 使用场景:用来表示一组明确的可选值列表 比如:贪吃蛇游戏,方向的可选值是上、下、左、右。
//字面量类型
function changeDirection(direction: 'up' | 'down' | 'left' | 'right') {
console.log(direction)
}
优势:相比于string类型,使用字面量更加精确、严谨
2.9 枚举类型
2.9.1 数字枚举
默认从0自增,也可以设定初始值,然后依次自增
//枚举
enum Direction { Up=10, Down, Left, Right }
function changeDirection(direction: Direction) {
console.log(direction)
}
changeDirection(Direction.Left) //打印:12
类似于js中的对象,直接使用点(.)语法访问枚举的成员。
2.9.2 字符串枚举
字符串枚举必须指定默认值
enum Direction {
up = 'up',
down = 'down',
left = 'left',
right = 'right'
}
function changeDirection(direction: Direction) {
console.log(direction)
}
changeDirection(Direction.up) //打印:up
2.10 any类型
不推荐使用any!这会让typeScript编程“AnyScript”,失去TS类型保护的优势
let obj: any = { x: 0 }
obj.bar = 100
obj()
const n: number = obj
//声明的时候没有指定类型,默认就是any类型
let a
a = 1;
a = ''
a()
//函数参数没有指定类型,默认就是any类型
function add(num1, num2) {
}
add(1, 2)
add(1, '2')
add(1, false)
2.11 typeof检测类型
检测变量或对象属性的类型
- 在类型注解中,获取ts的类型
- 在普通js中,获取js的类型
//js中的类型检测
console.log(typeof("北京"))
//ts的类型检测:在类型注解中,获取x和y的类型
let p = { x: 1, y: 2 }
function getPoint(point: typeof p) {
return point.x + point.y
}
let result = getPoint({ x: 1, y: 100 })
console.log(result)
//会报错,不能获取函数返回值的类型
let ret: typeof getPoint({ x: 1, y: 100 })
3.TS高级类型
3.1 class类
- 构造函数的类型约束
class Person {
//添加实例属性age和name
name: string = '张三'
age: number
//构造函数没有返回值类型
constructor(name: string, age: number) {
this.name = name;
this.age = age
}
}
let p = new Person("张三", 21)
console.log(p.name)
console.log(p.age)
- 普通方法的类型约束
class Point {
x = 10;
y = 10
scale(n: number) {
this.x *= n;
this.y *= n
}
}
let p = new Point()
p.scale(4)
console.log(p.x, p.y)
3.2 类的继承
//类的继承
class Animal {
move() {
console.log("我能跑步")
}
}
class Dog extends Animal {
bark() {
console.log("汪汪")
}
}
const dog = new Dog()
dog.move()
dog.bark()
3.3 类实现接口
//类实现接口
interface Singable {
sing(): void
}
//Person类中必须实现Singable中实现的sing方法
class Person implements Singable {
sing(): void {
console.log("你是我的小呀小苹果")
}
}
3.4 public、protected和private
- public:对当前类、子类和实例对象都可见
- protected:在当前类和子类可见,对实例对象不可见
- private:只在当前类中可见,对实例对象和子类不可见
//类的可见性
class Animal {
private __run__() {
console.log('Animal内部辅助函数')
}
protected move() {
this.__run__()
console.log("走两步")
}
public run() {
this.__run__()
this.move()
console.log("跑起来")
}
}
class Dog extends Animal {
bark(){
console.log("汪汪")
}
}
let d=new Dog();
d.run()
3.5 readonly
只能在constructor中赋值,不能在其他方法中赋值 只能修饰属性,不能修饰方法
class Person {
readonly username: string;
constructor(username: string) {
this.username = username
}
setName(name) {
this.username = name //报错:不能为只读属性赋值
}
}
- 对象也可以使用只读属性
//对象也可以使用只读属性
let obj: { readonly name: string } = {
name: "jack"
}
obj.name = "david" //不能为只读属性赋值
3.6 类型兼容性
两种类型系统
- 结构化类型系统:关注值所具有的形状
- 标明性类型系统
class Point { x: number; y: number }
class Point2D { x: number; y: number }
class Point3D { x: number; y: number; z: number }
const p: Point = new Point2D()
//成员多的可以赋值给成员少的
const p1: Point = new Point3D()
3.6.1 interface兼容性
成员多的可以赋值给成员少的
3.6.2 函数参数兼容性
1.简单的
//参数少的可以赋值给参数多的
type F1 = (a: number) => void
type F2 = (a: number, b: number) => void
let f1: F1
let f2: F2
// f2 = f1
// f1 = f2
f2 = f1 //参数
// f1 = f2 //参数少的可以赋值给参数多的
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 //函数中参数少的可以赋值给参数多的
3.6.3 函数返回值兼容性
返回值类型,只关注返回值类型本身即可
//原始类型
type F5 = () => string
type F6 = () => string
let f5: F5
let f6: F6
//对象类型
type F7 = () => { name: string }
type F8 = () => { name: string, age: number }
let f7: F7
let f8: F8
f7 = f8
3.7 交叉类型
交叉类型(&),用于组合多个类型为1个类型,常用语对象类型
//交叉类型
interface Person {
name: string
say(): number
}
interface Contact {
phone: string
}
type PersonDetail = Person & Contact
let obj: PersonDetail = {
name: "张三",
phone: "18301682292",
say() {
return 1
}
}
交叉类型和接口继承的对比: 相同:都可以实现对象类型的组合 不同:对于同名属性,处理类型冲突的方式不同
// 1.使用继承,会报错
interface A {
fn: (value: number) => string
}
interface B extends A {
fn: (value: string) => string
}
//2.使用&,不会报错
interface A {
fn: (value: number) => string
}
interface B {
fn: (value: string) => string
}
type C = A & B
let c:C
c.fn(100)
c.fn("张三") //传递number和string都可以
3.8 泛型
什么时候用泛型?让接口、函数、变量、参数等等,既具有类型多样性,又要保证类型安全,这时候就用泛型
function id<Type>(value: Type) { return value }
const num = id<number>(10)
const str = id<string>("你好")
const ret = id<boolean>(true)
解释:
- 语法:在函数名称后面添加<>(尖括号),尖括号中指定具体的类型,比如此处的number
- 当传入number后,这个类型就会被函数声明时指定的类型变量Type捕获到
- 此时,Type的类型就是number,所以函数id的参数和返回值类型也都是number
同样,如果传入类型string,函数id参数和返回值的类型都是string 同样,通过泛型就做到了让id函数与多种不同的类型一起工作,实现了复用的同时保证了类型安全
3.8.1 泛型中类型推断
简化调用泛型函数
function id<Type>(value: Type) { value }
const num = id(100)
const str = id("北京你好")
解释:
- 在调用泛型函数的时候,可以省略<类型>来简化函数的调用
- 此时,TS内部会蚕蛹一种叫做类型参数推断的机制,根据实参的类型自动推断出类型变量Type的类型
- 比如,传入实参10,TS会自动推断出num的类型number,并作为Type的类型
- 当编译器无法推断类型或者推断的类型不准确时,就需要显式地传入类型参数
3.8.2 类型约束
添加泛型的类型约束,主要有以下两种方式:1 指定更加具体的类型 2 添加约束
- 指定更加具体的类型
比如将类型改为Type[](Type类型的数组),因为只要是数据就一定存在length属性,因此就可以访问了。
//添加泛型的类型约束
function id<Type>(value: Type[]): Type[] {
return value
}
- 添加约束
//添加约束
interface ILength { length: number }
//Type满足Ilength的类型约束
function id<Type extends ILength>(value: Type) {
console.log(value.length)
return value
}
id(["a","b"]) //数组有length属性
id("北京你好") //字符串有length属性
解释:
- 创建描述约束的接口ILength,该接口要求提供length属性
- 通过extends关键字使用该接口,为泛型(类型变量)添加约束
- 该约束表示:传入的类型必须具有length属性
- 传入的实参只要有length属性即可,这也符合前面降到的类型兼容性
3.8.3 对类型变量约束
泛型的类型变量可以有多个,并且类型变量之间还可以约束(比如,第二个类型变量受第一个类型变量约束)。 创建一个函数涵获取对象中属性的值:
function getProp<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key]
}
let person = { name: 'jack', age: 18 }
getProp(person, 'name')
解释:
- 添加第二个类型变量Key,两个类型变量之间用(,)逗号分隔
- keyof关键字接收一个对象类型,生成其键的名称(可能是字符串或数字)的联合类型
- 本示例中keyof Type获取的是person对象所有键的联合类型,也就是“name|age”
- 类型变量Key受Type约束,可以理解为:Key只能是Type所有键中的任意一个,或者说只能访问对象中存在的属性
3.8.4 泛型接口
interface IdFunc<Type> {
id: (value: Type) => Type
ids: () => Type[]
}
let obj: IdFunc<number> = {
id(value) { return value },
ids() { return [1, 3, 5] }
}
解释:
- 在接口名称的后面添加<类型变量>,name,这个接口就变成了泛型接口
- 接口的类型变量,对接口中的所有其他成员可见,也就是接口中所有成员都可以使用类型变量
- 使用泛型接口时,需要显示指定具体的类型,(比如,此处的idFun)
- 此时,id方法的参数和返回值类型都是number,ids方法的返回值类型是number[]
3.8.5 泛型类
创建泛型类:
class GenericNumber<NumType>{
defaultValue: NumType
add: (x: NumType, y: NumType) => NumType
}
//指定泛型类的返回值类型
const myNum = new GenericNumber<number>()
myNum.defaultValue = 10
解释:
- 在class名称后面添加<类型变量>,这个类就编程了泛型类
- 此处的add方法,采用的是箭头形式的类型书写方法
class GenericNumber<NumType>{
defaultValue: NumType
add: (x: NumType, y: NumType) => NumType
constructor(value:NumType){
this.defaultValue=value
}
}
//可以省略<类型>,因为类型推断可以自己推断出来
const myNum = new GenericNumber()
myNum.defaultValue = 10
3.9 泛型工具类
Partial
类型可选
interface Props {
id: string
children: number[]
}
type PartialProps = Partial<Props>
let p1: Props = {
id: '',
children: [1]
}
let p2: PartialProps = {
id: '',
children: [1, 3]
}
ReadOnly
只读类型
interface Props {
id: string,
children: number[]
}
type ReadonlyProps = Readonly<Props>
let props: ReadonlyProps = {
id: "1",
children: [1, 2, 4]
}
props.id = "2" //这里会报错,只读属性不能赋值
Pick
Pick<Type,Keys>,从Type中选择一组属性来构造新类型
interface Props {
id: string
title: string
children: number[]
}
type PickProps = Pick<Props, 'id' | 'title'>
解释:
- Pick工具类型有两个变量:1.选择谁的属性 2.选择哪几个属性
- 第二个类型变量传入的属性,只能是第一个变量中存在的属性
- 头灶出来的PickProps,只有id和title两个属性类型
Record
泛型工具类 Record<Keys,Type>构造一个独享类型,属性键为Keys,属性类型为Type
// 1.使用Record工具
// type RecordObj = Record<'a' | 'b' | 'c', string[]>
// 2.使用传统方法
type RecordObj = {
a: string[]
b: string[]
c: string[]
}
let obj: RecordObj = {
a: ['a'],
b: ['b'],
c: ['c']
}
解释: Record工具类型有两个类型变量;1.对象有哪些属性 2.对象属性的类型 构建的新对象类型
3.10 签名类型
绝大多数情况下,我们都可以在使用对象前就确定对象的结构,并为对象添加准确的类型 使用场景:当无法确定对象中有哪些属性(或对象中可以出现任意多个属性),这时就用到了索引签名类型了
interface AnyObject {
[key: string]: number
}
let obj: AnyObject = {
a: 1,
b: 2
}
解释:
- 使用[key:string]来约束该接口中允许出现的属性名称。表示只要是string类型的属性名称,都可以出现在对象中。
- 对象obj中就可以出现任意多个属性(比如,a,b等)。
- key只是一个占位符,可以换成任意合法的变量名称
3.11 映射类型
映射类型1
基于旧类型创建新类型,减少重复,提升开发效率 比如:类型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 }
//映射类型
type Type2 = { [Key in PropKeys]: number }
//映射类型只能在类型别名中使用,不能在接口中使用
interface Type3 {
[Key in PropKeys]: number
}
注意:
- 映射类型是基于索引签名类型,该类型类似于索引签名类型,也使用了[]
- Key in PropKeys表示key可以使PropKeys联合类型中的任意一个,类似于for in /(let key in)
- 注意:映射类型只能在类型别名中使用,不能在接口中使用
映射类型2
根据一个对象类型,创建另一个对象类型
type type1 = { a: number, b: number, c: number }
type type2 = { [key in keyof type1]: number }
let obj: type2 = {
a: 100,
b: 200,
c: 300
}
注意:
- keyof type1获取到type1中所有键的联合类型,即 'a'|'b'|'c'
- Key in...就表示Key可以使type1中所有的键名称中的任意一个
Partial的实现
泛型工具类型(比如:Partial)都是基于映射类型实现的
type MyPartial<T> = {
[P in keyof T]?: T[P]
}
type Props = { a: number, b: string, c: boolean }
type Props1 = MyPartial<Props>
let obj: Props1 = {
a: 100,
b: "你好"
}
解释:
- keyof Props表示获取Props的所有键,也就是'a'|'b'|'c'
- 在[]后面添加?(问号),表示讲这些属性变为可选的,以此来实现Partial的功能
- 冒号后面的T[P]表示获取T中的每个键对应的类型,比如,如果'a'则类型是number;如果'b'则类型是string
- 最终type1和props类型完全相同,只是让所有类型变为可选了
索引类型
同时查询索引的多个类型
type Props = { a: number, b: string, c: boolean }
//TypeA和TypeB是一样的
type TypeA = Props['a' | 'b'|'c']
type TypeB = Props[keyof Props]
4.类型声明文件
创建.d.ts文件 类型也可以暴露和引入
// type.d.ts文件
type Props = {
a: number, b: string
}
export { Props }
//index.ts文件
import { Props } from "./type";
let obj: Props = {
a: 100,
b: "hello world"
}
5.react项目
React脚手架工具:create-react-app(简称:CRA),默认支持TypeScript
创建TS的项目命令:npx create-react-app 项目名称 --template typescript`
react-app-env.d.ts文件
react-app-env.d.ts:React项目默认的类型声明文件
/// <reference types="react-scripts" />
解释:告诉TS帮我加载react-script这个包提供的类型声明 react-script类型声明文件包含两部分类型
- react、react-dom、node的类型
- 图片、样式等模块的类型,以允许在代码中导入图片、svg等文件
TS会自动加载.d.ts文件,以提供类型声明(通过修改tsconfig.json中的include配置来验证)
TS配置文件tsconfig.json
作用:指定项目文件和项目编译所需的配置项 tsconfig.json文件生成命令:tsc --init
- 直接编译文件,将忽略tsconfig.json文件:tsc hello.ts --target es6,
- 使用tsconfig.json编译文件:tsc
{
//编译选项
"compilerOptions": {
//生成代码的语言版本
"target": "es5",
//指定药包含在编译中的library
"lib": ["dom", "dom.iterable", "esnext"],
//允许ts编译器编译js文件
"allowJs": true,
//跳过声明文件的类型检查
"skipLibCheck": true,
//es模块互操作,定比TSModule和CommonJS之间的差异
"esModuleInterop": true,
//通过允许import x from 'y'即使模块没有显式指定default导出
"allowSyntheticDefaultImports": true,
//开启严格模式
"strict": true,
//对文件名称强制区分大小写
"forceConsistentCasingInFileNames": true,
//为switch语句启用错误报告
"noFallthroughCasesInSwitch": true,
//生成代码的模块化标准
"module": "esnext",
//模块解析(查找)策略
"moduleResolution": "node",
//允许导入扩展名为.json的模块
"resolveJsonModule": true,
//是否将没有import/export的文件视为旧(全局而非模块化)脚本文件
"isolatedModules": true,
//编译时不生成任何文件(只进行类型检查)
"noEmit": true,
//指定将JSX编译成什么模式
"jsx": "react-jsx"
},
//指定允许ts处理的目录
"include": ["src"]
}
/src/App.vue
import { FC } from "react";
type Props = { name: string; age?: number }
//创建Hello组件
const Hello: FC<Props> = ({ name, age }) => (
<div>
你好,我叫:{name},我今年{age}岁
</div>
)
//设置默认值
Hello.defaultProps = {
age: 18
}
//创建App组件
const App = () => (
<div>
<Hello name="张三" age={11}></Hello>
</div>
)
export default App;
只使用TS
import { FC } from "react";
type Props = { name: string; age?: number }
//完全按照函数在TS中的写法
const Hello = ({ name, age=18 }: Props) => (
<div>
你好,我叫:{name},我今年{age}岁
</div>
)
const App = () => (
<div>
<Hello name="张三" age={11}></Hello>
</div>
)
export default App;
事件类型
/src/App.vue
import React, { FC } from "react";
type Props = { name: string; age?: number }
//创建Hello组件
const Hello = ({ name, age }: Props) => {
//点击事件
const onClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log("赞!", e.currentTarget)
}
//input的change事件
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value)
}
return (
<div>
你好,我叫:{name},我{age}岁了
<button onClick={onClick}>点赞</button>
<input onChange={onChange} />
</div>
)
}
const App = () => (
<div>
<Hello name="张三" age={11}></Hello>
</div>
)
export default App;