这是我参与「第五届青训营 」伴学笔记创作活动的第 6 天
今日学习了TypeScript的基本使用,由于是第一次接触的TypeScript,所以不仅学习了课上的知识,还去其他地方补了基础知识,如TypeScript新增的数据类型、接口以及泛型。
1.TypeScript的运行环境
◼ 在前面我们提到过,TypeScript最终会被编译成JavaScript来运行,所以我们需要搭建对应的环境
- 我们需要在电脑上安装TypeScript,这样就可以通过TypeScript的Compiler将其编译成JavaScript
- 所以,我们需要先可以先进行全局的安装
# 安装命令
npm install typescript -g
# 查看版本
tsc --version
# 编译ts文件
tsc math.ts
◼ 如果我们每次为了查看TypeScript代码的运行效果,都通过经过两个步骤的话就太繁琐了:
- 第一步:通过tsc编译TypeScript到JavaScript代码;
- 第二步:在浏览器或者Node环境下运行JavaScript代码;
◼ 上面我提到的两种方式,可以通过两个解决方案来完成:
- 方式二:通过ts-node库,为TypeScript的运行提供执行环境;
- 方式一:通过webpack,配置本地的TypeScript编译环境和开启一个本地服务,可以直接运行在浏览器上
方案一:通过ts-node库
#方式二:安装ts-node
npm install ts-node -g
#另外ts-node需要依赖 tslib 和 @types/node 两个包
npm install tslib @types/node -g
#现在,我们可以直接通过 ts-node 来运行TypeScript的代码:
ts-node math.ts
方案二:webpack搭建typeScipt运行环境
- npm init创建package.json(初始化package.json)
- 下载npm install webpack webpack-cli -D(下载webpack)
- npm install ts-loader -D(配置ts的loader)
- npm install html-webpack-pugin -D(对index.html进行打包处理)
- npm install webpack-dev-server -D(创建本地服务)
const path = require('path')
const HTMLWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: "development",
entry: "./src/index.ts",
output: {
filename: "hundle.js",
path: path.resolve(__dirname, './build')
},
resolve: {
extensions: ['.ts', '.js', 'cjs', '.json']
},
module: {
rules: [
{
test: /.ts$/,
loader: "ts-loader"
}
]
},
plugins: [
new HTMLWebpackPlugin({
title: 'ts',
template: './index.html'
})
]
}
2.TypeScript的数据类型
any类型
let a: any = "why"
a = 123
a = true
const aArray: any[] = ["why", 18, 1.88]
unknown类型
用法上和any差不多,但是也有所差别
any 类型的变量可以赋值给任意变量。
unknow 类型的变量,只能赋值给any和unknow类型,可以理解为any的类型安全。
void类型
function foo(): void {
console.log('没有返回值的函数')
}
如果函数没有放回值,可以不写返回类型,默认放回void
tuple类型
const myInfos: [string, number, number] = ['lyh', 18, 1.88]
const item1 = myInfos[0] //'lyh',并且知道类型是string类型
const item2 = myInfos[1] //18, 并且知道类型是number类型
那么tuple和数组有什么区别呢?
首先,数组中通常建议存放相同类型的元素,不同类型的元素是不推荐放在数组中。(可以放在对象或者元组 中)
其次,元组中每个元素都有自己特性的类型,根据索引值获取到的值可以确定对应的类型;
枚举类型
enum Direction {
LEFT,
RIGHT,
TOP,
BOTTOM
}
function turnDirection(direction: Direction) {
switch (direction) {
case Direction.LEFT:
console.log('左')
break
case Direction.RIGHT:
console.log('右')
break
case Direction.TOP:
console.log('上')
break
case Direction.BOTTOM:
console.log('下')
break
default:
const myDirection: never = direction
}
}
turnDirection(Direction.LEFT)
字面量类型
let message: "Hello World" = "Hello World"
默认情况下这么做是没有太大的意义的,但是我们可以将多个类型联合在一起
type AlignType = 'left' | 'right' | 'center'
function changeAlign(align: AlignType) {
console.log("修改发现":align)
}
changeAlign('left')
类的类型
class Person {
name: string
constructor(name: string) {
this.name = name
}
}
const p1: Person = new Person("why")
const p2: Person = {
name: "kobe",
running: function() {
console.log(this.name + "running")
}
}
3.TypeScript类型补充
可选类型
对象和函数参数可以指定哪些属性是可选的,可以在属性后面添加一个?
// 可选类型必须也在必选类型后面
function printPoint(x: number, y: number, z?: number) {
console.log(x, y, z)
}
printPoint(10, 20)
type infoType = {
name: string,
family: {
name: string,
age?: number
}
}
const info: infoType = {
name: '佩奇',
family: {
name: '乔治',
}
}
联合类型
// 联合类型中的每一个类型被称之为联合成员
function foo(id: string | number) {
console.log(id)
}
foo(10)
foo('lyh')
交叉类型
type nameObjType = {name: string}
type ageObjType = {age: number}
const info: nameObjType & ageObjType = {
name: 'lyh',
age: 18
}
类型别名
type infoType = {
name: string,
family: {
name: string,
age?: number
}
}
const info: infoType = {
name: '佩奇',
family: {
name: '乔治',
}
}
类型断言as
有时候TypeScript无法获取具体的类型信息,这个我们需要使用类型断言
比如我们通过 document.getElementById,TypeScript只知道该函数会返回 HTMLElement ,但并不知道它具体的类型 (代码如下)
const myEl = document.getElementById("my-img") as HTMLImageElement
myEl.src = "图片类型"
TypeScript只允许类型断言转换为 更具体或者不太具体的类型版本,此规则可防止不可能的强制转换,在非必要的情况下尽量少使用此方法。 (代码如下)
const name = ("coderwhy" as unknown) as number
非空类型断言!
非空断言使用的是 ! ,表示可以确定某个标识符是有值的,跳过ts在编译阶段对它的检测
function printMessage(message?: string) {
console.log(message!.toUpperCase)
}
printMessage('Coder')
空值合并操作符 (??)和逻辑或(||)的区别
空值合并操作符(??): 只有当左侧为null和undefined时,才会返回右侧的数
逻辑或操作符(||): 只有当左侧通过!!会转化为false类型,才会返回右侧的数
类型缩小
① typeof
function foo(arg: number | string) {
if (typeof arg === 'string') {
arg.toUpperCase
}
console.log(arg)
}
② 平等缩小
type align = 'left' | 'center' | 'right'
function foo(arg: align) {
switch (arg) {
case 'left':
console.log('left')
break
case 'center':
console.log('center')
break
case 'right':
console.log('right')
}
}
③ instanceof
class Programmer {
coder() {
console.log('写代码')
}
}
class Teacher {
study() {
console.log('学习')
}
}
function move(arg: Programmer | Teacher) {
if (arg instanceof Programmer) {
arg.coder()
}else {
arg.study()
}
}
④ in
type FishType = {
name: string,
swimming: () => void
}
type DogType = {
name: string,
runing: () => void
}
function move(animal: FishType | DogType) {
if ('swimming' in animal) {
animal.swimming()
}else {
animal.runing()
}
}
const fish:FishType = {
name: '鱼儿',
swimming() {
console.log('游泳')
}
}
move(fish)
剩余参数
function sum(...arr: number[]) {
return arr.reduce((prev, item) => prev + item, 0)
}
console.log(sum(1,2,3))
不确定的this类型
function sayHello() {
console.log(this.name)
}
const info = {
name: 'why',
sayHello
}
info.sayHello()
上面这段代码会报错:
- 这里我们再次强调一下,TypeScript进行类型检测的目的是让我们的代码更加的安全;
- 所以这里对于 sayHello 的调用来说,我们虽然将其放到了info中,通过info去调用,this依然是指向info对象的;
- 但是对于TypeScript编译器来说,这个代码是非常不安全的,因为我们也有可能直接调用函数,或者通过别的对象来 调用函数
所以应该指定this的类型
type NameType = {
name: string
}
function sayHello(this: NameType) {
console.log(this.name)
}
const info = {
name: 'why',
sayHello
}
info.sayHello()
3.TypeScript函数
函数表达式的类型
type sumType = (arg1: number, arg2: number) => number
const sum: sumType = function(num1: number, num2: number): number {
return num1 + num2
}
函数类型的参数个数问题
问题引出:明明bar中只有一个参数,calcFn中却要求传入两个参数,为什么bar传入不会报错??
interface ICalcFn {
(n1: number, n2: number) : void
}
function calc(calcFn: ICalcFn) {
const num1 = 10
const num2 = 20
const res = calcFn(num1, num2)
console.log(res)
}
const foo = function(num1: number, num2: number): number {
return num1 + num2
}
const bar = function(num1: number): number {
return num1
}
calc(foo)
calc(bar) //明明bar中只有一个参数,calcFn中却要求传入两个参数,为什么bar传入不会报错??
这是因为如果限制了函数参数的数量,数组掉调用forEach的时候就要完整的传入item、index、arr三个参数。这样会导致ts变的不太好用。
函数的调用签名
// 函数的调用签名
interface IBar {
name: string
age: number
(num: number): number
}
const bar: IBar = (num: number): number => {
return 123
}
bar.name = 'aaa'
bar.age = 18
bar(123)
函数的构造签名
class Person {
}
interface ICTORPerson {
new(): Person
}
function factory(fn: ICTORPerson) {
const f = new fn()
return f
}
factory(Person)
函数的重载
function sum(a1: number, a2: number): number
function sum(a1: string | number, a2: string): string
function sum(a1: string, a2: string | number): string
function sum(a1: any, a2: any): any {
return a1 + a2
}
函数体执行前,会先看一下声明类型部分有没有匹配,匹配才会执行函数体。
在可能的情况下,尽量选择使用联合类型来实现
4.TypeScript面向对象
类的继承
我们使用extends关键字来实现继承,子类中使用super来访问父类。
- 我们来看一下Student类继承自Person:
-
- Student类可以有自己的属性和方法,并且会继承Person的属性和方法;
- 在构造函数中,我们可以通过super来调用父类的构造方法,对父类中的属性进行初始化;
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
name: string
age: number
running() {
console.log('跑步')
}
}
class student extends Person {
constructor(name, age, id) {
// 调用父类的构造器
super(name, age)
this.id = id
}
id: number
}
类的多态
多态的目的是为了写出更具通用性的代码
class Animal {
active() {
console.log('行走')
}
}
class Fish extends Animal {
active(): void {
console.log('游动')
}
}
class Dog extends Animal {
active(): void {
console.log('跑动')
}
}
// 父类引用(类型)指向子类对象
function getActives(animals: Animal[]) {
animals.forEach(animal => {
animal.active()
})
}
getActives([new Fish(), new Dog()])
类的成员修饰符
- 在TypeScript中,类的属性和方法支持三种修饰符: public、private、protected
-
- public 修饰的是在任何地方可见、公有的属性或方法,默认编写的属性就是public的;
- private 修饰的是仅在同一类中可见、私有的属性或方法;可以通过方法get和set私有的属性
class Persion {
private name: string = '刘德华'
getName() {
return this.name
}
setName(name: string) {
this.name = name
}
}
const p = new Persion()
const res = p.getName()
console.log(res)
-
- protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法;
class Persion {
protected name: string = '刘德华'
}
class Boy extends Persion {
constructor() {
super()
}
getName() {
return this.name
}
}
const cxk = new Boy()
const res = cxk.getName()
console.log(res)
只读属性 readonly
如果有一个属性我们不希望外界可以任意的修改,只希望确定值后直接使用,那么可以使用readonly,在constructor中可以修改
getters/setters
在前面一些私有属性我们是不能直接访问的,或者某些属性我们想要监听它的获取(getter)和设置(setter)的过程, 这个时候我们可以使用存取器。
class Person {
private _name: string = '蔡徐坤'
get name() {
return this._name
}
set name(newValue) {
this._name = newValue
}
}
const p = new Person()
p.name = '刘德华'
console.log(p.name)
静态成员
class Person {
static fullName: string = '蔡徐坤'
static age: number = 18
}
console.log(Person.fullName)
console.log(Person.age)
抽象类abstract
- 我们知道,继承是多态使用的前提。
-
- 所以在定义很多通用的调用接口时, 我们通常会让调用者传入父类,通过多态来实现更加灵活的调用方式。
- 但是,父类本身可能并不需要对某些方法进行具体的实现,所以父类中定义的方法,,我们可以定义为抽象方法。
- 什么是抽象方法? 在TypeScript中没有具体实现的方法(没有方法体),就是抽象方法。
-
- 抽象方法,必须存在于抽象类中;
- 抽象类是使用abstract声明的类;
- 抽象类有如下的特点:
-
- 抽象类是不能被实例的(也就是不能通过new创建);
- 抽象方法必须被子类实现,否则该类必须是一个抽象类;
// 抽象类
abstract class shape {
abstract getArea()
}
// 矩形类
class Rectangle extends shape {
constructor(weight: number, height: number) {
super()
this.weight = weight
this.height = height
}
weight: number
height: number
getArea( ) {
return this.weight * this.height
}
}
// 圆形类
class Circular extends shape {
constructor(radius: number) {
super()
this.radius = radius
}
radius: number
getArea() {
return 3.14 * (this.radius ** 2)
}
}
// 矩形实例
const r = new Rectangle(10, 20)
// 圆形实例
const c = new Circular(10)
function makeArea(shape: any) {
return shape.getArea()
}
console.log(makeArea(r), makeArea(c))
5.TypeScript接口
接口的声明
interface IInfoType {
name: string
age: number
}
const info: IInfoType = {
name: 'why',
age: 18
}
只读属性
interface IInfoType {
readonly name: string
age: number
}
const info: IInfoType = {
name: 'why',
age: 18
}
索引签名
interface IBookType {
[index: number]: string
}
const info: IBookType = {
0: 'HTML',
1: 'JavaScript',
2: 'CSS',
}
接口继承
- 接口和类一样是可以进行继承的,也是使用extends关键字:
-
- 并且我们会发现,接口是支持多继承的(类不支持多继承)
interface Person {
name: string
eating: () => void
}
interface Animal {
runing: () => void
}
interface Student extends Person, Animal {
id: number
}
const student:Student = {
id: 111,
name: 'why',
eating() {
console.log('吃饭')
},
runing() {
console.log('跑步')
}
}
接口类的实现
interface Iswim {
swimming: () => void
}
interface Irun {
running: () => void
}
class Person implements Iswim, Irun {
swimming() {
console.log('游泳')
}
running() {
console.log('跑步')
}
}
interface和type区别
- 我们会发现interface和type都可以用来定义对象类型,那么在开发中定义对象类型时,到底选择哪一个呢?
-
- 如果是定义非对象类型,通常推荐使用type,比如Direction、Alignment、一些Function;
- 如果是定义对象类型,那么他们是有区别的:
-
-
interface 可以重复的对某个接口来定义属性和方法;
-
而type定义的是别名,别名是不能重复的;
-
字面量赋值( freshness擦除 )
第一次创建的对象字面量,称之为fresh(新鲜)
对于新鲜的字面量,会进行严格的类型检查,必须完全满足类型的要求(不能有多余的属性)
interface IInfo {
name: string
age: number
sing: () => void
}
const obj = {
name: '蔡徐坤',
age: 18,
sing() {
console.log('游泳')
},
hobby() {
console.log('跳舞')
}
}
// 将一个 变量标识符 赋值给其他的变量时,会进行freshness擦除操作。
const info: IInfo = obj
6.TypeScript泛型
泛型实现类型参数化
function sum<Type>(num: Type) {
return num
}
sum<number>(2)
泛型传入多个类型
function sum<T, E>(num: T, str: E) {
return num
}
sum<number, string>(2, '123')
泛型接口
interface IInfo<T> {
name: string
age: number
hobby: T
}
const info: IInfo<() => void> = {
name: '蔡徐坤',
age: 18,
hobby() {
console.log('rap')
}
}
泛型类
class Person<T, E> {
name: T
age: E
constructor(name: T, age: E) {
this.name = name
this.age = age
}
}
const cxk = new Person<string, number>('蔡徐坤', 18)
console.log(cxk)
泛型约束
interface IgetLength {
length: number
}
function getLength<T extends IgetLength>(arg: T) {
return arg.length
}
const res = getLength<string>('你好,李银河')
console.log(res)//6