TypeScript简介
- TypeScript由微软开发,基于JS的扩展语言
- TS包含了JS的所有内容,即TS是JS的超集
- TS增加了静态类型检查、接口,泛型等很多现代开发特性,因此适合大型项目的开发。
- TS需要编译为JS,然后交给浏览器或者其他JS运行环境执行。
为什么需要TS
今非昔比的JS
- JS当年诞生的定位是浏览器的脚本语言,用于在网页中嵌入一些简单的逻辑,而且代码量很少,随着时间的推移,JS变得越来越流行,如今的JS已经可以全栈编程了。
- 现如今的JS的应用场景比当年丰富的多,代码量也比当年大的多,随便一个JS项目的代码量,可以轻松的达到几万行,甚至十几万行。
- 然而JS当年"出生简陋",没考虑到如今的应用场景和代码量,逐渐的就出现了很多困扰。
JS的困扰
不清不楚的数据类型
scss
let welcome = 'hello'
welcome() // 此时报错 TypeError:welcome is not a function
有漏洞的逻辑
python
const str = Date.now() % 2 ? '奇数' :'偶数'
if (str !== '奇数'){
alert('hello')
}else if (str === '偶数'){
alert('world')
}
访问不存在的属性
arduino
const obj = {width:10,height:15}
const area = obj.width * obj.heigth // heigth 不存在,应该为height
低级的拼写错误
arduino
const message = 'hello,world'
message.toUperCase() // toUperCase少写了一个p,应该是toUpperCase
【静态类型】检查
- 在代码运行前进行检查,发现代码的错误或不合理之处,减小运行时异常的出现的几率,此种检查叫【静态类型检查】,TypeScript和核心就是【静态类型检查】,简言之就是把运行时的错误前置。
- 同样的功能,TypeScript的代码量要大于JS,但由于TS的代码结构更加清晰,在后期代码的维护中TS却远胜于JS。
编译TypeScript
浏览器不能直接运行TypeScript代码,需要编译为JavaScript再交由浏览器解析器执行。
类型声明
typescript
let a: String
let b: number
let c: boolean
a = 'hello'
b = -99
c = true
function count(x:number,y:number):number{
return x + y
}
let result = count(1,2)
console.log(result)
类型推断
TS会根据我们的代码,进行类型推导,例如下面代码中的变量d,只能存储数字
ini
let d = -99 // TypeScript 会推断出变量的d的类型是数字
d = false // 警告:不能将类型'boolean'分配给类型'number'
但要注意,类型推断不是万能的,面对复杂类型时推断容易出问题,所以尽量还是明确的编写类型声明!
类型总揽
JS中的数据类型
string,number,boolean,null,undefined,bigint,symbol,object
其中object包含 Array,Function,Date,Error等。。。
TypeScript中的数据类型
- 上述所有的JavaScript类型
- 六个新类型 any,unknown,never,void,tuple,enum
- 两个用于自定义类型的方式 type,interface
typescript
let str1: string // TS官方推荐的写法,只能值类型的字符串,不能写字符串形式的包装对象。
str1 = 'hello'
str1 = new String('hello')
let str2: String // 可以接受值类型的字符串,也能写字符串形式的包装对象。
str2 = 'hello'
str2 = new String('hello')
注意点⚠️
在JS中这些内置构造函数:Number、String、Boolean,它们用于创建对应的包装对象,在日常开发时很少使用,在TypeScript中也是同理,所以在TypeScript中进行类型声明时,通常都是写小写的number、string、boolean。
- 原始类型VS包装对象
- 原始类型:如number、string、boolean,在JS中是简单数据类型,它们在内存中占用空间少,处理速度快
- 包装对象:如Number对象,String对象,Boolean对象,是复杂类型,在内存中占用更多空间,在日常开发时很少由开发人员自己创造包装对象。
- 自动包装:JS在必要时会自动将原始类型包装成对象,以调用方法或访问属性。
javascript
// 原始类型字符串
let str = 'hello'
//当访问str.length时,JS引擎做了一下工作:
let size = (function(){
//1.自动装箱:创建一个临时的String对象包装原始字符串
let tempStringObject = new String(str)
//2.访问String对象的lenth属性
let lengthValue = tempStringObject.length
//3.销毁临时对象,返回长度值
//(JS引擎自动处理对象销毁,开发者无感知)
return lengthValue;
})
常用类型
any
any的含义是:任意类型,一旦将变量类型限制为any,那就意味着放弃了对改变量的类型检查
ini
//明确的表示a的类型时any --- 【显式的any】
let a:any
//以下对a的赋值,均无警告
a = 100
a = '你好'
a = false
//没有明确的表示b的类型是any,但TS主动推断出来b是any -- 隐式的any
let b
//以下对b的赋值,均无警告
b = 100
b = '你好'
b = false
注意点;any类型的变量,可以赋值给任意类型的变量
typescript
/*注意点;any类型的变量,可以赋值给任意类型的变量*/
let c:any
c = 9
let x:string
x = c // 无警告 x = 9
unknown
unknown 的含义是:未知类型
- unknown可以理解为一个类型安全的any,适用于:不确定数据的具体类型
ini
//设置a的类型unknown
let a:unknown
//以下对a的赋值,均正常
a = 100
a = false
a = '你好'
// 设置x的数据类型为string
let x:string
x = a // 警告:不能将类型'unknown'分配给类型'string'
- unknown会强制开发者在使用之前进行类型检查,从而提供更强的类型安全性
ini
// 设置x的数据类型为string
let a:unknown
a = 'hello'
// 第一种
if(typeof a === 'string'){
x = a
}
// 第二种(断言)
x = a as string
// 第三种方式:加断言
x = <string>a
- 读取any类型数据的任何属性都不会报错,而unknown正好与之相反
typescript
let str1:string
str1 = 'hello'
str1.toUpperCase() // 无警告
let str2:any
str2 = 'hello'
str2.toUpperCase() // 无警告
let str3:unknown
str3 = 'hello'
str3.toUpperCase() // 警告,'str3'的类型为'未知'
// 使用断言强制指定str3的类型为string
(str3 as string).toUpperCase() // 无警告
never
never的含义是:任何值都不是,简言之就是不能有值,undefined、 null 、''、0 都不行
- 几乎不用never去直接限制变量,因为没有意义,例如:
ini
//指定a的类型为never,那就意味着a以后不能存任何的数据了
let a:never
// 以下对a的所有赋值都会有警告
a = 1
a = true
a = undefined
a = null
- never一般是TypeScript主动推断出来的,例如
typescript
// 指定a的类型为string
let a:string
// 给a设置一个值
a ='hello'
if (typeof a === 'string'){
console.log(a.toUpperCase())
}else {
console.log(a) // TS会推断出此处的a是never,因为没有任何一个值符合此处的逻辑
}
- never也可用于限制函数的返回值(一直调用循环,或者抛出异常的函数)
typescript
//限制throwError函数不需要有任何返回值,任何值都不行,像undefined、null都不行
function throwError(str:string):never {
throw new Error('程序异常退出:'+ str)
}
void
- void通常用于函数返回值声明,含义:【函数返回值为空,调用者也不应依赖其返回值进行任何操作】
c
function logMessage(msg:string):void {
console.log(msg)
}
logMessage('你好')
注意 :编码着没有编写return去指定函数的返回值,所以logMessage函数是没有显式返回值的,但是会有一个隐式返回值,就是undefined;即:虽然函数返回类型为void,但也是可以接受undefined的,简单记:undedined 是void可以接受的一种'空' 以下写法均符合规范
javascript
//无警告
function logMessage(msg:string):void{
console.log(msg)
}
//无警告
function logMessage(msg:string):void{
console.log(msg)
return;
}
//无警告
function logMessage(msg:string):void{
console.log(msg)
return undefined;
}
那限制函数返回值时,是不是undefined和void就没区别呢? --- 有区别,因为还有句话说:【返回值类型void的函数,调用者不应依赖其返回值进行任何操作!】对比下面两段代码:
c
fuction logMessage(msg:string):void{
console.log(msg)
}
let result = logMessage('你好')
if (result){ // 此行报错,无法测试'void'类型的表达式的真实性
console.log('logMessage有返回值')
}
scss
fuction logMessage(msg:string):undefined{
console.log(msg)
}
let result = logMessage('你好')
if (result){ // 此行无警告
console.log('logMessage有返回值')
}
理解void与undefined
- void是一个广泛的概念,用来表达'空',而undefined则是这种'空'的具体实现之一。
- 因此可以说undefined是void能接受"空"的状态的一种具体形式
- 换句话说:void包含undefined,但void表达的语义超越了单纯的unedined,它是一种意图上的约定,而不仅仅是特定值的限制
总结:若函数返回类型为void,那么;
- 从语法上讲:函数是可以返回undefined的,至于显式返回,还是隐式返回,这无所谓!
- 从语义上讲:函数调用者不应关心函数返回的值,也不应依赖返回值进行任何操作!即使返回了undefined值。
object
关于object与Object,直接说结论;实际开发中用的相对较少,因为范围太大了。
object(小写)
object 小写的含义是:所有的非原始类型,可存储:对象、函数、数组等,由于限制的范围比较宽泛,在实际开发中使用的相对较少。
ini
let a:object // a的值可以是任何【非原始类型】,包括,对象,函数,数组等
//以下代码,是将【非原始类型】赋给a,所以均符合要求
a = {}
a = {name:'张三'}
a = [1,3,5,7,9]
a = new String('123')
class Person{}
a = new Person()
// 以下代码,是将【原始类型】赋给a,有警告
a = 1 // 警告:不能将类型'number'分配给类型'object'
a = true // 警告:不能将类型'boolean'分配给类型'object'
a = '你好' // 警告:不能将类型'string'分配给类型'object'
a = null // 警告:不能将类型'null'分配给类型'object'
a = undefiend // 警告:不能将类型'undefined'分配给类型'object'
Object(大写)
- 官方描述:所有可以调用Object方法的类型
- 简单记忆:除了undefined和null的任何值
- 由于限制的范围实在太大了!所以实际开发中使用频率极低
ini
let a:Object // b能存储的类型是可以调用到Object方法的类型
b = {}
b = {name:'张三'}
b = [1,3,5,7,9]
b = new String('123')
class Person{}
b = new Person()
b = 1
b = true
b = '你好'
// 这两个是存不了的
// b = null
// b = undeinfed
声明对象类型
- 实际开发中,限制一般对象,通常使用以下形式
typescript
//限制person1对象必须有name属性,age为可选属性
let person1:{name :string,age?:number}
// 含义同上,也能用分号做分割
let person1:{name :string;age?:number}
// 含义同上,也能用换行做分割
let person3:{
name:string
age?:number
}
//如下赋值均可以
person1 = {name:'李四',age:18}
person2 = {name:'张三'}
person3 = {name:'王五'}
// 如下赋值不合法,因为person3的类型限制中,没有对gender属性的说明
person4 = {name:'王五',gender:'男'}
- 索引签名:允许定义对象可以具有
任意数量的属性
,这些属性的键
和类型
是可变的
,常用于:描述类型不确定的属性(具有动态属性的对象)
typescript
//限制person对象必须有name属性,可选age属性但值必须是数字,同时可以有任意数量、任意类型。
let person:{
name:string,
age?:number,
[key:string]:any // 索引签名,完全可以不用key这个单词,换成其他的也可以。
}
//赋值合法
person = {
name:'张三',
age:18,
gender:'男'
}
声明函数类型
typescript
let count:(a:number,b:number) => number
count = function(x,y){
return x + y
}
备注:
- TypeScript中的 => 在函数类型声明时表示函数类型,描述其参数类型和返回类型
- JavaScript中的 => 是一种定义函数的语法,是具体的函数实现
- 函数类型还可以使用:接口、自定义类型等方式,下文中会详细讲解
声明数组类型
ini
let arr1:string[]
let arr2:Array<string>
arr1 = ['a','b','c']
arr2 = ['hello','world']
备注:上述代码中的Array<string>
属于泛型,下文会详细讲解
tuple
元祖(Tuple)是一种特殊的数组类型,可以存储固定数量的元素,并且每个元素的类型是已知的且可以不同。元祖用于精确描述一组值的类型, ?表示可选元素。
ini
// 第一个元素必须是string类型,第二个元素必须是number类型
let arr1:[string,number]
//第一个元素必须是number类型,第二个元素是可选的,如果存在,必须是boolean类型
let arr2:[number,boolean?]
//第一个元素必须是number类型,后面的元素可以是任意数量的string类型
let arr3:[number,...string]
// 可以赋值
arr1 = ['hello',123]
arr2 = [100,false]
arr3 = [200]
arr3 = [100,'hello','world']
arr3 = [100]
//不可以赋值,arr1声明时是两个元素,赋值的是三个
arr1 = ['hello',123,false]
enum
枚举(enum)可以定义一组命名常量
,它能增强代码的可读性,也让代码更好维护
数字枚举
数字枚举一种最常见的枚举类型,其成员的值会自动递增
,且数字枚举还具备反向映射
的特点,在下面代码的打印中,不难发现:可以通过值
来获取对应的枚举成员名称
。
scss
//定义一个描述【上下左右】方向的枚举Direction
enum Direction {
Up,
Down,
Left,
Right
}
cosole.log(Direction) // 打印Direction 会看到如下内容
/*
0:'Up',
1:'Down',
2:'Left',
3:'Right',
Up:0,
Down:1,
Left:2,
Right:3
*/
//反向映射
console.log(Direction.Up)
console.log(Direction[0])
//此行代码报错,枚举中的属性是只读的
Direction.Up = 'shang'
也可以指定枚举成员的初始值,其后的成员值会自动递增
scss
enum Direction {
Up = 6,
Down,
Left,
Right
}
console.log(Direction.Up) // 6
console.log(Direction[1]) // 7
使用数字枚举完成刚才walk函数中的逻辑,此时我们发现:代码更加直观易读,而且类型安全,同时也更易于维护
scss
enum Direction {
Up,
Down,
Left,
Right
}
function walk(n:Direction){
if (n === Direction.Up){
console.log('向上走')
}else if (n == Direction.Down){
console.log('向下走')
}else if (n == Direction.Left){
console.log('向左走')
}else if (n == Direction.Right){
console.log('向右走')
}
}
walk(Direction.Up)
walk(Direction.Down)
字符串枚举
ini
enum Direction {
Up = 'up',
Down = 'down',
Left = 'left',
Right = 'right'
}
let dir:Direction = Direction.Up
console.log(dir) // 输出;'up'
常量枚举
官方描述:常量枚举是一种特殊枚举类型,它使用const 关键字定义,在编译时会被內联,避免生成额外的代码
type
type可以为任意类型创建别名,让代码更简洁、可读性更强,同时能更方便地进行类型复用和扩展。
1. 基本用法
类型别名使用type 关键字定义,type后跟类型名称,例如下面代码中 num 时类型别名
ini
type num = number
let price:num
price = 100
2. 联合类型
联合类型是一种高级类型,它表示一个值可以是几种不同类型之一。
lua
type Status = number | string
type Gender = '男' | '女'
function printStatus(status:Status){
console.log(status)
}
function logGender(str:Gender){
console.log(str)
}
3. 交叉类型
交叉类型(Intersection Types)允许将多个类型合并为一个类型,合并后的类型将拥有所有被合并类型的成员。交叉类型通常用于对象类型。
typescript
type Area = {
height:number; // 高
width:number;// 宽
}
//地址
type Address = {
num:number // 楼号
cell:number // 单元号
room:string // 房间号
}
type House = Area & Address
const house:House = {
height:100, // 高
width:100, // 宽
num:3, // 楼号
cell:4, // 单元号
room:'702' // 房间号
}
属性修饰符
修饰符 | 含义 | 具体规则 |
---|---|---|
public | 公开的 | 可以被:类内部、子类、类外部访问 |
protected | 受保护的 | 可以被:类内部、子类访问 |
private | 私有的 | 可以被:类内部访问 |
readonly | 只读属性 | 属性无法修改 |
public修饰符
typescript
class Person {
//name 写了public修饰符,age没写修饰符,最终都是public修饰符
public name:string
age:number
constructor(name:string,age:number){
this.name = name
this.age = age
}
speak(){
//类的内部可以访问public修饰的name和age
console.log('我叫:${this.name},今年${this.age}岁')
}
}
const p1 = new Person('张三',18)
//类的【外部】可以访问public修饰的属性
console.log(p1.name)
scala
class Student extends Person {
constructor(name:string,age:number){
super(name,age)
}
study(){
// 【子类中】可以访问父类中的public修饰的:name属性,age属性
console.log('今年${this.age}岁的${this.name}正在努力学习')
}
}
属性的简写形式
简写前
typescript
class Person {
//name 写了public修饰符,age没写修饰符,最终都是public修饰符
public name:string
age:number
constructor(name:string,age:number){
this.name = name
this.age = age
}
}
简写后
typescript
class Person {
constructor(public name:string,public age:number){}
}
protected修饰符
typescript
class Person {
constructor(protected name:string,protected age:number){}
protected getDetails(){
return `我叫:${this.name},今年${this.age}岁`
}
introduce(){
console.log(this.getDetails())
}
}
const p1 = new Person('张三',18)
p1.introduce() // public 可以在外部访问
// p1.name 在外部不能访问
// p1.getDetails 在外部不能访问
scala
class Student extends Person {
constructor(name:string,age:number){
super(name,age)
}
study(){
this.getDetails()
console.log('${this.name}正在努力学习')
}
}
const s1 = new Student('tom',1)
s1.study()
private修饰符
typescript
class Person {
constructor(name:string,age:number,privated IDCard;string){}
private getPrivateInfo(){
return `身份证号码为:${this.IDCard}`
}
getInfo(){
return `我叫:${this.name},今年${this.age}岁`
}
getFullInfo(){
return this.getInfo() + ',' + this.getPrivateInfo()
}
}
readonly修饰符
typescript
class Person {
constructor(name:string,readonly age:number){}
}
抽象类
抽象类不能实例化
,可以被继承
,抽象类里有普通方法
,也可以有抽象方法
。
typescript
abstract class Package {
// 构造方法
constructor(public weight:number){}
// 抽象方法
abstract calcuate():number
//具体方法
printPackage(){
console.log('包裹重量为:${this.weight}kg,运费为:${
this.calcuate()}元')
}
}
scala
class StandardPackage extends Package {
constructor(weight:number,public unitPrice:number){super(weight)}
calcuate():number {
return this.weight * this.uniPrice
}
}
const s1 = new StandardPackage(10,5)
s1.printPackage()
kotlin
class ExpressPackage extends Package {
constructor(weight:number,public unitPrice:number,public additional:number){super(weight)}
calcuate():number {
if (this.weight > 10){
return 10 * this.uniPrice + (this.weight - 10) * this.uniPrice
}else {
return this.weight * this.uniPrice
}
}
}
const s1 = new ExpressPackage(13,8,2)
s1.printPackage()
总结:何时使用抽象类
- 定义通用接口:为一组相关的类定义通用的行为(方法或属性)时。
- 提供基础实现:在抽象类中提供某些方法或为其提供基础实现,这样派生类就可以继承这些实现。
- 确保关键字实现:强制派生类实现一些关键行为
- 共享代码和逻辑:当多个类需要共享部分代码时,抽象类可以避免代码重复。
interface 接口
interface 是一种定义结构
的方式,主要作用是为:类、对象、函数等规定一种契约
,这样可以确保代码的一致性和类型安全,但要注意interface只能
定义格式
,不能
包含任何实现
!
定义类结构
typescript
// PersonInterface接口,用于限制Person类的格式
interface PersonInterface接口 {
name:string
age:number
speak(n:number):void
}
// 定义一个类Person,实现PersonInterface接口
class Person implements PersonInterface {
constructor(public name:string,public age:number){}
//实现接口的speak方法,注意:实现speak时参数个数可以少于接口中的规定,但不能多
speak(n:number):void {
for (let i = 0;i < n ;i++) {
//打印出包含名字和年龄的问候语句
console.log(`我叫:${this.name},今年${this.age}岁`)
}
}
}
// 创建一个Person类的实例p1,传入名字'tom'和年龄18
const p1 = new Person('tom',18)
p1.speak(3)
定义对象结构
typescript
interface UserInterface {
name:string
readonly gender:string // 只读属性
age?:number // 可选属性
run:(n:number) => void
}
const user:UserInterface = {
name:'张三',
gender:'男',
age:18,
run(n) {
console.log('奔跑了${n}米')
}
}
定义函数结构
typescript
interface CountInterface {
(a:number,b:number):number;
}
const count:CountInterface = (x,y) => {
return x + y
}
接口之间的继承
一个interface 继承另一个interface,从而实现代码的复用
typescript
interface PersonInterface {
name:string // 姓名
age:number // 年龄
}
interface StudentInterface extends PersonInterface {
grade:string // 年级
}
const stu:StudentInterface = {
name:'张三',
age:17,
grade:'高三'
}
接口自动合并(可重复定义)
typescript
interface PersonInterface {
name:string // 姓名
age:number // 年龄
}
interface PersonInterface {
grade:string // 年级
}
总结:何时使用接口?
- 定义对象的格式:描述数据模型,API响应格式、配置对象。。。等等你,是开发中用的最多的场景。
- 类的契约;规定一个类需要实现哪些属性和方法
- 自动合并:一般用于扩展第三方库的类型,这种特性在大型项目中可能用到。
一些相似概念的区别
interface 与 type 的区别
- 相同点:interface 和 type 都可以用于定义
对象结构
,两者在许多场景中是可以互换的。 - 不同点:
- interface:更关注于定义
对象
和类
的结构,支持继承、合并
- type:可以定义
类型别名、联合类型、交叉类型
,但不支持继承和自动合并。
- interface:更关注于定义
interface 和 type 都可以用于定义对象结构
typescript
// 使用 interface 定义 Person对象
interface PersonInterface {
name:string; // 姓名
age:number; // 年龄
speak():void;
}
// 使用type 定义 Person 对象
type PersonType = {
name:string;
age:number;
speak():void;
}
let p1:PersonInterface = {
name:'tom',
age:18,
speak(){
cosole.log(this.name)
}
}
interface 可以继承、合并
typescript
interface PersonInterface {
name:string // 姓名
age:number // 年龄
}
interface PersonInterface {
speak:() => void
}
type 的交叉类型
interface 与抽象类的区别
- 相同点:都用于定义一个类的格式(应该遵循的契约)
- 不同点:
- interface:只能描述结构。不能有任何实现代码,一个类可以实现多个接口,用逗号','分隔。
- 抽象类:既可以包含抽象方法,也可以包含具体方法,一个类只能继承一个抽象类
泛型
泛型允许我们在定义函数、类或接口时,使用类型参数来表示未指定的类型
,这些参数在具体使用时,才被指定具体的类型
,泛型能让同一代码适用于多种类型,同时仍然保持类型的安全性
泛型函数
kotlin
function logData<T>(data:T):T {
console.log(data)
return data
}
logData<number>(100)
logData<string>('hello')
泛型可以有多个
typescript
function logData<T,U>(data:T,data2:U) : T | U {
console.log(data1,data2)
return Date.now() % 2 ? data1 :data2
}
logData<number,string>(100,'hello')
logData<string,boolean>('ok','false')
泛型接口
typescript
interface PersonInterface<T> {
name:string,
age:number,
extraInfo:T
}
let p1:PersonInterface<string>
let p2:PersonInterface<number>
p1 = {name:'张三',age:18,extraInfo:'一个好人'}
p2 = {name:'李四',age:18,extraInfo:250}
泛型约束
typescript
interface PersonInterface<T> {
name:string,
age:number
}
function logPerson<T extends PersonInterface>(info:T):void {
console.log(`我叫:${this.name},今年${this.age}
}
logPerson({name:'张三',age:18})
泛型类
csharp
class Person<T> {
constructor(
public name:string,
public age:number,
public extraInfo:T,
){}
speak(){
console.log(`我叫:${this.name},今年${this.age}
console.log(this.extraInfo)
}
}
// 测试代码1
const p1 = new Person<number>('tom',30,250)
// 测试代码2
type JobInfo = {
title:string,
company:string
}
const p2 = new Person<JobInfo>('tom',30,{title:'研发总监',company:'xxxx科技公司'})
类型声明文件
类型声明文件是TypeScript中的一种特殊文件,通常以.d.ts作为扩展名,它的主要作用是为现有的JavaScript代码提供类型信息
,使得TypeScript能够在使用这些JavaScript库或模块时进行类型检查和提示
demo.js
css
export function add(a,b){
return a + b
}
export function mul(a,b){
return a * b
}
demo.d.ts
typescript
declare function add(a:number,b:number):number;
declare function mul(a:number,b:number):number;
export {add,mul}