初识TypeScript

708 阅读12分钟

写在前面

本文是针对于初学TypeScript的同学所编写的一文,在这篇文章里,我会尽量从新手角度思考以及去提问问题(我自己也是个新手)并解决疑问,并且会带上一些重要的知识点带你快速入门TypeScript,但是并不会太深入 ,我目的只是想推动你学习TypeScript,以上。

TypeScript是啥

这里不说太官方的定义,它本质就是一门给JavaScript提供静态校验的编译语言,同时自身也有强大的ES6支持,它会严格检查你代码每一处,减少你的报错概率~

我学它的意义在哪

我们都知道,JavaScript是一门动态语言,所有变量,对象,乱七八糟的在运行时类型很不稳定,你如果想完全的确定某个类型还必须得加上if else判断,最常见的情况是我们经常会遇到读取某个变量undefined,XXX方法不存在等等这些,它的意义就存在了,下面总结几点好处

  • 它可以检查你的代码,可以在运行之前就捕获你的错误
  • 如果你学会了TypeScript,那么你写的代码会带上纯天然的注释效果,配合IDE的功能,你会得到很棒的代码提示效果!这个在团队合作发挥很大的作用!(这个可是大重点)
  • 对ES6支持得很好,学完加薪

搭建一个易于学习的环境

首先是安装TypeScript

npm install -g typescript

然后我们编写TypeScript文件后缀名改为ts,使用"tsc"终端命令编译文件,就能得到一个js文件

这是我们编译后的js文件,是给浏览器跑的

但是我们为了方便学习,不需要这样麻烦都要去编译然后放到浏览器执行,跟着如下步骤进行操作

第一步

npm install ts-node -g

第二步

vscode编辑器上搜索插件"Code Runner"安装 直接在ts文件右键

至此环境搭建完成

基础篇

声明基础类型

我们先来学习怎么声明基础类型变量

let num:number = 1 // 声明数字类型
let bol:boolen = true // 声明布尔值
let str:string = 'str' // 声明字符串
let nul:null = null // 声明null
let undef:undefined = undefined // 声明undefined

当声明确定好类型之后,之后的赋值操作只能根据定义的类型来赋值否则会报错

值得一提的是,undefined和null这两个特殊类型是所有类型的子类型(不包含void),怎么理解这句话呢,就是当你声明基础类型了之后去赋值这任意两种类型都是可以正常编译的

// 以下这两种赋值都是不会报错的
let num:number = undefined
let str:string = null 

TypeScript还提供了一种JavaScript没有的概念类型"void",它代表着空值,但是我个人觉得没啥用,因为它声明了只能赋值为undefinednull

let u:void = undefined 
let n:void = null

包容性也不是很好,下面代码会报错

它更多时候是用于表达一个函数没有返回值

//如果加了void加上了return关键字就会报错
function fn():void {
    cosole.log('')
}

任意值

一个包容性极强的类型,也是很多人把ts当成js写的重要类型any,它可以表示赋值为任意类型

// 以下代码不会报错
let a:any = 'str'
a = true
a = 10

可以看到我们的代码已经失去了静态检查的意义回归了JavaScript,变成了anyScript

如果你在声明一个变量没有指定类型,那么这个变量就会任意类型,所以为了我们使用ts提供的静态类型检查咱少用any就少用,记住我们学ts的初心:良好的类型检查,提前发现错误,给代码添加注释,方便你我他

类型接口

接口是在面向对象语言当中一个很重要的概念,具体表现形式是定义接口和实现接口。

TypeScript 也是有提供类型接口的定义的,我们看下简单例子

interface Person {
    name: string;
    age: number;
}

let tom: Person = {
    name: 'Tom',
    age: 25
};

上面的例子我们通过interface 关键字声明了一个名为"Person"的接口,tom对象对Person进行了实现,现在他们是一致,但是如果tom对象少了接口中的某个属性或者多了某个属性就会报错。

但是我们也不希望接口那么死板,我们可不可以把接口变得宽容一些呢?可以的!

可选参数

interface Person {
    name: string;
    age?: number; // 通过"?"号对参数添加可选属性
}

let tom: Person = {
    name: 'Tom'
}; // 即使少了age属性ts也不会报错

但是如果上述代码新增了一个Person没有的属性ts还是会报错,如果想让接口多出一个任意属性我们可以这么写

interface Person {
    name: string;
    age?: number;
    [propName: string]: any; // 通过propName指定键类型,右边赋值任意属性的类型
}

let tom: Person = {
    name: 'Tom',
    age: 18,
    gender: '男'
};

有一点要特别注意!一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集(这句话是阮一峰老师讲的),吧上面代码改成下面这样会报一个错

interface Person {
    name: string;
    age?: number;
    [propName: string]: string; // 改成string
}

let tom: Person = {
    name: 'Tom',
    age: 18,
    gender: '男'
};

报错截图

根据解释来讲,我们的任意属性类型为string而我们的可选属性却是number, number并不是string的子集,也就报错了,解决办法有三种,第一种就是把age的类型改为undefined或者null,因为string的子集就是含有undefinednull,但是这个就很蠢!!!!第二种我们可以使用联合类型创造一个"父集",这样问题就解决了~,第三种使用any....

interface Person {
  name: string
  age?: number
  [propName: string]: string | number // 联合类型
}

只读属性

如果你希望你的接口类型里某些对象只能用于读取,可以使用readonly 关键字声明

interface IProp {
    readonly id: number;
    color: string;
}

let prop:IProp = {
    id: 12345
    color: 'red'
}
prop.id = 54321 // 报错信息:Cannot assign to 'id' because it is a constant or a read-only property. 

看起来有点const 的意思,但它其实比const 还要严格,即使你声明只读属性是一个对象,那么之后对这个对象任何更改都是不允许的, const却可以,毕竟 const 保护的对象的引用内存地址~

声明数组

数组的声明有很多种方法,也比较灵活

最简单的 "类型+[]"

let narr:number[] = [1,2,3,4] // 声明只能含有number类型的数组
let sarr:string[] = ['1','2','3'] // 声明只能含有string类型的数组

/* 并且在我们调用数组方法的时候也会进行类型检查 */
narr.push('5') // 报错:类型"5"的参数不能赋给类型"number"的参数

数组泛型

这里我们先简单演示用法,后面再单独讲泛型

语法是Array

let arr:Array<number> = [1,2,3,4,5] 

接口数组

interface NumberArr {
  [index: number]: number
}
let arr:NumberArr = [1,2,3]

这段代码表示的是只要索引的类型是数字的时候,那么值的类型必须是数字

但是这种方法用来表述数组简直过于秀技,interface一般是用来描述特殊形状的东西,像比如我们的类数组arguments

function fn() {
    let args:number[] = arguments // 报错信息:Type 'IArguments' is missing the following properties from type 'number[]': pop, push, concat, join, and 24 more.
}

可以看到无论我们用什么形式的类型数组去描述 arguments 都是不正确的,这个时候可以利用接口去定义它的形状

interface ArgumentsArr {
    [index:number]:number|any
    length:number
    callee:Function
}

function fn () {
  let args: ArgumentsArr = arguments
}

但其实TypeScript也给我们提供好了arguments的类型校验

function fn () {
  let args: IArguments = arguments // IArguments已经内置直接使用即可
}

函数声明

js声明函数有两种方法

// 函数声明(Function Declaration)
function sum(x, y) {
    return x + y;
}

// 函数表达式(Function Expression)
let mySum = function (x, y) {
    return x + y;
};

我们使用typescript定义函数要记住一点,必须要定义函数的输入于输出,这将是项目灵魂之一

函数声明类型验证

function sum(x:number, y:number):number {
    return x + y
}
sum(1,2) // 此时实参必须一个不漏的并且类型也按照约定传递,少一个都是不行的

函数表达式类型验证

let sum:(x:number,y:number) => number = function (x: number, y: number): number {
    return x + y; 
}

可能同学会有疑问,怎么函数表达式要写两坨一样的东西,还变麻烦了,是这样的,其实你这样写也是能得到类型检查的,就算你不写,ts背地里也是完成了一个叫类型推论的东西,但是还是建议不要省这点代码,定义接口到实现接口这是一个很重要的面向对象思想

// 不会报错也能得到类型验证
let mySum: (x: number, y: number) => number = function (x, y) {
  return x + y;
};

而更多时候我们都是采用接口来定义函数的形状

interface SplitStr {
  (str: string): string[]
}

let mySearch: SplitStr
mySearch = function (str:string):string[] {
  let strs = str.split('')
  return strs
}

定义函数的可选参数

我们前面的例子都是1:1的参数,不传还会报错,那么有没有什么方法让参数是可传可不传的呢

有的,只要才参数后面加上'?'即可

function fn (num: number, str?: string): string | number {
  if (str) {
    return str + num
  } else {
    return num
  }
}
console.log(fn(1)) // 1
console.log(fn(1,'2')) // 12

但要注意的是,可选参数必须定义在必选参数后面,下面这种情况会报错

function fn(str?:string,num:number): string | number {} // 报错:必选参数不可位于可选参数后面

基础总结

我们现在来把我们所学的来"注释"一个节流函数尝尝鲜

JavaScript

const save = (function () {
  let time = null
  return function (callback, second) {
    if (time) return
    time = setTimeout(() => {
      callback.apply(null)
      time = null
    }, second)
  }
})()

TypeScript

interface ISave {
  /** callback:回调函数  second: 毫秒数*/  // <= 加上注释之后会形成天然的提示效果
  (callback: () => void | any, second: number): void
}

const save: ISave = (function () {
  let time: null | number = null // 这里要给联合类型number是因为setTimeout方法返回值是一个number类型,不加会ts报错
  return function (callback: () => void | any, second:number) {
    if (time) return
    time = setTimeout(() => {
      callback.apply(null)
      time = null
    }, second)
  }
})()

接下来是使用的提示

就很棒对吧~~

进阶篇

元组

简单可以理解为一个确定每一项元素类型和长度的数组

上代码 一目了然

let tuple: [string, number] = ['str', 25]

tuple[0].split('') // 正常调用
tuple[1].toFixed(2) // 正常调用

// 报错用法
tuple[0] = 1 // 不能将类型'1'赋给string类型

简单介绍一下了,毕竟n多场景我是没想到元组要用在哪(有知道的同学可以讨论讨论)

枚举

使用枚举可以定义一些带名字的常量,这个在项目当中也比较常用。

数字枚举

使用enum关键字声明枚举类型

enum Nums {
  ONE, TOW, THREE, FOUR, FIVE, SIX, SEVEN
}
console.log(Nums)
/*
打印结果
{
  '0': 'ONE',
  '1': 'TOW',
  '2': 'THREE',
  '3': 'FOUR',
  '4': 'FIVE',
  '5': 'SIX',
  '6': 'SEVEN',
  ONE: 0,
  TOW: 1,
  THREE: 2,
  FOUR: 3,
  FIVE: 4,
  SIX: 5,
  SEVEN: 6
}
*/

能分析出来,在我们声明了Nums枚举类型后,变成了一个对象,并且1比1的相互映射了每一个成员的值

数字枚举如果没有定义初始值的话,他会默认从0开始递增赋值给每一位成员,我们也发现上面的枚举类型并不准确,数字和名字没有相互对应

这个时候我们只需要给第一位赋一次初始值就行

enum Nums {
  ONE = 1, TOW, THREE, FOUR, FIVE, SIX, SEVEN
}
/*
{
  '1': 'ONE',
  '2': 'TOW',
  '3': 'THREE',
  '4': 'FOUR',
  '5': 'FIVE',
  '6': 'SIX',
  '7': 'SEVEN',
  ONE: 1,
  TOW: 2,
  THREE: 3,
  FOUR: 4,
  FIVE: 5,
  SIX: 6,
  SEVEN: 7
}
*/

成员的值会从1开始逐渐递增

但是如果你非要不按套路来,给TOW也赋初始值为1 ,那么后续成员都是从1开始递增,也就是后续成员默认值取决于前一位的初始值

这个数字枚举还是很常用的,举个例子,我们可以通过枚举来定义我们的网络请求返回CODE

enum RequestCode {
  SUCCEES = 200,
  ERROR = 500
}

if(res.code === RequestCode['SUCCEES']) {...}

这样的话代码语义化又变强了些

字符串枚举

除了可以给成员赋值也可以使用枚举一些常量字符串,这个比较简单,具体的应用可以给redux的action的type拟定一些常量

enum ActionType {
    ADD = "ADD",
    REMOVE = "REMOVE",
    REPACE = "REPACE",
    UPDATE = "UPDATE",
}

泛型

这里引用阮一峰老师的解释

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

这句话被翻译的很好

函数泛型

摘个例子,现在有个函数有如下需求:创建一个指定长度的数组,每一项都填充一个默认值

function createArray(length: number, defaultValue: any): Array<any> {
    let result = [];
    for (let i = 0; i < length; i++) {
        result[i] = defaultValue;
    }
    return result;
}
createArray(3,'x')

这个时候调用函数没问题,但是小伙伴们想一想,我函数的返回值包括默认值都是定义的any,而我的函数很有可能是多个人来调用,比如A同事希望调用这个函数得到一个长度为3的,默认值为number类型的数组,这样会使函数没法得到类型校验,想得到校验还得跑去函数改掉anynumber,那么这个时候B同事又来调用这个函数希望得到一个长度为4,默认值为布尔值类型的数组....这样没完没了改不是问题啊

泛型就是来解决这个的问题的!

function createArray<T>(length: number, defaultValue: T): Array<T> {
  let result = [];
  for (let i = 0; i < length; i++) {
      result[i] = defaultValue;
  }
  return result;
}
createArray<string>(3,'xxx')
let strs = []
createArray<string>(3,'xxx').forEach(item => {
  strs.push(item.split('')) // 得到了类型验证,正常调用字符串api
})
console.log(strs); // [ [ 'x', 'x', 'x' ], [ 'x', 'x', 'x' ], [ 'x', 'x', 'x' ] ]

上面的例子我们在函数后面定义了一个<T> ,这个T就是用来动态代替类型的变量,这个小尖括号<>里定义了的变量直接使用到你想指定动态类型的参数后面即可,然后在调用的时候同时使用小尖括号指定你的类型,这个类型就会替换掉<T>里的T!

接口泛型

接口同样也支持泛型,我们我们在定义一个函数或者对象形状时都可以使用泛型提高灵活性

来改改上面的代码

// 先定义接口
interface CreateArrayFunc<T> {
	(length:number,value:T): Array<T> // 或者T[]
}
// 使用接口并装载类型
let createArray:CreateArrayFunc<string>
// 实现函数
createArray = function<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}
let strs = []
createArray(3,'xxx').forEach(item => {
  strs.push(item.split('')) // 得到了类型验证,正常调用字符串api
})
console.log(strs); // [ [ 'x', 'x', 'x' ], [ 'x', 'x', 'x' ], [ 'x', 'x', 'x' ] ]

同样,你如果熟悉es6的类写法,类也是支持定义泛型的

class Person<T, S> {
  name: T
  age: S
  constructor (name: T, age: S) {
    this.name = name
    this.age = age
  }
  getInfo () {
    console.log(this.name, this.age)
  }
}

let p = new Person<string, number>('张三', 19)

泛型也是同时声明多个,用逗号隔开就好,是不是就跟函数差不多

总结

感谢您的耐心阅读,以上就是TypeScript我觉得入门需要了解的一些知识点,至少他们是很常用的,如果有小伙伴觉得读着哪里有疑问可以和我交流,现在时间还蛮多,我会帮忙处理你学习TypeScript上遇到的一些问题,看影响我可以在写一点进阶一点的装饰器相关的内容

参考