本文已参与「新人创作礼」活动,一起开启掘金创作之路。
关于TypeScript在前端开发中的作用在这里就不再赘述了,下面我们来直接开干。
一、TypeScript基本使用
- 创建一个名为ts的空目录;
- 进入目录在终端执行npm init --yes初始化项目;
- 执行npm install typescript --save-dev,安装typescript到当前项目开发依赖;
npm install typescript --save-dev
- 创建01_getting_started.ts文件,typescript是javascript的超集,它本身支持javascript所有最新特性,所以在这里不使用类型系统直接写js也是不会有任何问题(vscode本身支持typescript语法);
function say (name: string): void {
console.log(`hello, ${name}.`)
}
say('Jack')
- 执行npx tsc 01_getting_started.ts,编译代码;
npx tsc 01_getting_started.ts
编译完成后会看到一个同名的js文件;
function say(name) {
console.log("hello, ".concat(name));
}
say('Jack');
二、配置文件
虽然tsc命令可以指定编译的目录,但我们一般情况会使用配置文件方式进行编译项目。
- 在终端运行npx tsc --init以创建ts配置文件(tsconfig.json);
npx tsc --init
打开配置文件会看到里面只有一个compilerOptions属性,里面大多数配置选项都是被注释掉的,这里我们先来看一些常用的配置选项。
- target:把ts编译成哪个版本的ECMAScript;
- module:以ESModule规范还是Commonjs规范进行编译;
- rootDir:要编译的目标目录;
- outDir: 要编译输出的目标目录;
- sourceMap:编译的同时是否生成sourceMap文件;
- strict:是否开启严格模式;
- lib:引用哪些标准库;
{
"compilerOptions": {
"target": "es2015",
"module": "commonjs",
"rootDir": "src",
"outDir": "dist",
"sourceMap": true,
"strict": true,
"lib": ["es2015", "es2016", "es2017", "es2018", "DOM"],
}
- 有了配置文件后,在终端运行npx tsc就会自动将src下的所有ts文件进行编译并输出到dist目录下。
npx tsc
三、原始数据类型
在非严格模式(或只开启StrictNullChecks)下stirng,number,boolean都可以为空。
void类型在严格模式下只能存放undefined,非严格模式下可以存放null或undefined。
Symbol是es2015标准中定义的成员(包括其他es2015标准定义的成员), 如果配置文件中target不是指定为es2015。 那么使用它前必须要确保有对应的es2015标准库的引用, 也就是说tsconfig.json中lib选项必须包含es2015("lib": ["es2015"])。 注意:自定义lib选项后默认的配置就会被覆盖掉,会导致console等对象无法使用。 解决办法是把它加回去("lib": ["es2015", "DOM"]),ts中DOM和BOM都被归总到一个标准库文件当中,就叫DOM。
const str: string = 'football'
const num: number = 168 // NaN Infinity
const bool: boolean = true // false
/*
在非严格模式(或只开启StrictNullChecks)下stirng,number,boolean都可以为空
const str: string = null
const num: number = null
const bool: boolean = null
*/
const v: void = undefined // void类型在严格模式下只能存放undefined,非严格模式下可以存放null或undefined
const n: null = null
const u: undefined = undefined
/*
Symbol是es2015标准中定义的成员(包括其他es2015标准定义的成员),
如果配置文件中target不是指定为es2015。
那么使用它前必须要确保有对应的es2015标准库的引用,
也就是说tsconfig.json中lib选项必须包含es2015("lib": ["es2015"])。
注意:自定义lib选项后默认的配置就会被覆盖掉,会导致console等对象无法使用,
解决办法是把它加回去("lib": ["es2015", "DOM"]),ts中DOM和BOM都被归总到一个标准库文件当中,就叫DOM。
(标准库:内置对象所对应的声明文件)
*/
const s: symbol = Symbol()
四、作用域问题
在我们练习ts的过程中难免会在不同文件中声明相同的全局变量,那么在ts编译的过程中就会出现重复声明变量这样一个错误。
解决这个问题有两种方法:
- 使用立即执行函数,把变量放在函数作用域内;
- 将当前文件作为模块进行导出;
// 使用立即执行函数
(function () {
const str: string = 'string'
})()
// 将文件作为模块导出,这里我们使用ESModule导出
const num: number = 200
export {} // 注意,这里并不是导出一个对象,只是export的语法,想要导出一个对象可使用export default {}
五、Object类型
ts中的Ojbect类型不是单指对象类型,它是泛指所有的非原始类型,也就是对象、数组和函数。
在这里如果我们想要定义一个变量只能接收对象类型的值,可以用字面量类型的方式注解。当然给一个对象定义类型最好的方式还是使用接口的方式。
// ojbect类型可以接收对象、数组、函数类型的值
const foo: object = function () {} // [] // {}
// 字面量类型的方式注解对象类型
const obj: { name: string, age: number } = { name: 'Jack', age: 27 } // 值必须跟定义的一样不能多也不能少
六、数组类型
ts中定义数组有两种方式(与Flow几乎相同),一种是使用Array泛型,第二种是使用元素类型加中括号。
// Array泛型定义纯数字组成的数组
const arr1: Array<number> = [1, 2, 3]
// 元素类型加[]定义纯数字组成的数组
const arr2: number[] = [4, 5, 6]
// 应用:接收剩余参数的时候不用像js那样要做一大堆类型判断
function sum (...args: number[]) {
return args.reduce((pre, cur) => pre + cur, 0)
}
sum(1, 2, 3)
七、元组类型
元组就是明确元素数量以及元素类型的一个数组,各个元素的类型可以不用完全相同。
在ts中可以使用数组字面量这种方法去定义元组。
元组一般用在一个函数中要返回多个返回值。
const tuple: [number, string] = [200, 'okay'] // 值必须与字面量定义的类型和数量一致
// 访问等操作与数组一样
console.log(tuple[0]) // 200
console.log(tuple[1]) // okay
const [status, message] = tuple
八、枚举类型
在ts中使用 enum 变量名 {} 去定义一个枚举类型变量。
枚举类型在编译的过程中会入侵我们代码,它实际上会编译一个双向的键值对对象(就是可以通过键去获取值也可以通过值去获取键)。
enum PostStatus {
Draft = 0,
Unpublished = 1,
Published = 2
}
// 如果枚举成员的值不指定的话,就会按照默认方式从0开始累加
enum PostStatus {
Draft,
Unpublished,
Published
}
// 如果第一个成员指定了值,后面的成员就会在该值的基础上进行累加
enum PostStatus {
Draft = 1,
Unpublished,
Published
}
// 字符串枚举(不常用),因为字符串无法进行累加,所以必须给每个成员指定一个字符串的值
enum PostStatus {
Draft = 'foo',
Unpublished = 'bar',
Published = 'baz'
}
console.log(PostStatus[0]) // Draft
console.log(PostStatus.Draft) // 0
// 这里如果我们不想使用索引的方式访问枚举,我们可以在定义常量枚举(前面加const)
const enum Status {
success,
fail
}
九、函数类型
因在js中有两种声明函数的方式,一种是函数声明的方式,一种函数表达式的方式。
// 函数声明的方式
function fn1 (a: number, b: number): string {
return 'fn1'
}
fn1(1, 2)
// 注意在ts中参数个数也必须要与声明的保持一致,不像js那像可以随便传递
// fn1(1)
// fn1(1, 2, 3)
// 可选参数,在形参后添加问号
function fn1 (a: number, b?: number): string {
return 'fn1'
}
// 可选参数,也可以使用ES6的参数默认值,因为形参指定了默认值它也就是可有可无的了
// 要注意的是不管是可选参数还是有默认值的参都必须放到最后
function fn1 (a: number, b: number = 200): string {
return 'fn1'
}
// 函数表达式的方式
// 它也可以以相同的方式去定义函数的参数与返回值的类型,
// 不过接收这个函数的变量它应该也要有对应的类型,
// 定义方式与箭头函数类似:(a: number, b: number) => string
const fn2: (a: number, b: number) => string = function (a: number, b: number): string {
return 'fn2'
}
十、任意类型
任意类型any,由于ts是基于js的,在js中有一些api接收的参数是可以任意类型。那么在ts中定义any类型的变量它跟js里一样该变量还是动态类型。
function stringfiy (val: any): string {
return JSON.stringfiy(val)
}
stringfiy('string')
stringfiy(200)
stringfiy(true)
// any类型是不安全的,尽量不要使用
let foo: any = 'foo'
foo = 100
foo.bar() // 在语法层面上不会报错
十一、隐式类型推断
在ts中如果我们没有给一个变量进行类型注解,那么ts就会自动跟据值进行类型推断,如果ts无法推断一个变量的类型它就会推断为any类型。
let num = 200 // 隐式推断为number类型
num = 'str' // TypeError
let foo // 没有进行赋值,所以被推断为any类型
foo = 100
foo = 'str'
十二、类型断言
有时候ts无法知道变量的类型,但我们在开发中是明显知道它的类型时可以使用类型断言告诉ts该变量是什么类型。
在变量后加 as 类型名称 或 在变量前加 <类型名称>。
// 假设该数组是从一个接口中获取的
const nums = [1, 2, 3]
const res = num.find(i => i > 0)
// const square = res * res
const num1 = res as number
const num2 = <number>res // 在jsx下不能使用
十三、接口
接口是一种规范,可以去约定对象的结构,去使用接口我们就要遵守接口全部的约定。
使用interface进行定义,注意接口里的成员可以使用逗号进行分割,但标准的方式是使用分号进行分割(这里的分号跟js里分号一样可以省略)
interface Post {
title: string
content: string
subtitle?: string // 可选成员,实际上就相当于给这个成员标记为string或undefined
readonly summary: string // 只读成员,初始化完成后不能修改
}
function printPost (post: Post): void {
console.log(post.title)
console.log(post.content)
}
printPost({
title: 'hello',
content: 'typescript sample'
})
interface Cache {
// 动态成员,注意这里key并不是固定的它只一个格式
[key: string]: string // 定义了键和值都是字符串类型的动态成员
}
const cache: Cache = {}
cache.foo = 'val1'
cache.bar = 'val2'
十四、类的基本使用
类就是可以用来描述一类具体事物的抽象特征。在ts中它除了可以使用所有ECMAScript类的所有功能外,它还有提供了一些额外的功能。
- 在ts中我们需要明确在类型当中去声明它所拥有的属性,而不是在构造函数中通过this去动态添加;
- 注意的是在ts中类的属性它必须要有一个初始值,使用等号去赋值或在构造函数中去初始化;
class Person {
// 在ts中我们需要明确在类型当中去声明它所拥有的属性,而不是在构造函数当中通过this去动态添加
// 注意的是在ts中类的属性它必须要有一个初始值,使用等号去赋值或在构造函数中去初始化
name: stirng // 可用等号赋值一个初始值 ='init name'
age: number
constructor (name: string, age: number) {
this.name = name
this.age = age
}
sayHi (msg: string): void {
console.log(msg)
}
}
十五、类的访问修饰符
类的访问修饰符可以去控制类属性的可访问级别。
- private:私有属性只能在当前类的内部去访问修改;
- public:公有属性,默认情况下属性加不加public都是公有的,但在开发中建议加上,这样的话代码会更容易理解;
- protected:受保护属性,跟private不同它允许在子类中访问修改
class Person {
public name: stirng
private age: number // 私有属性
protected gender: boolean // 受保护属性
constructor (name: string, age: number) {
this.name = name
this.age = age
this.gender = true
}
sayHi (msg: string): void {
console.log(msg)
}
}
class Student extends Person {
constructor (name: string, age: number) {
super(name, age)
console.log(this.gender) // protected属性可以在子类中访问修改
}
}
class Student2 extends Person {
// 注意构造函数的修饰符默认也是public,如果改成private它就不能在外部被实例化了
private constructor (name: string, age: number) {
super(name, age)
console.log(this.gender) // protected属性可以在子类中访问修改
}
static of (name: string, age: number): Student2 {
return new Student2(name, age)
}
}
const tom: Person = new Person('tom', 18)
console.log(tom.name)
// console.log(tom.age) // 无法在外部访问
// console.log(tom.gender) // 无法在外部访问
const jack: Student2 = Student2.of('jack', 27)
十六、类的只读属性
在ts中类属性的访问级别我们除了可以用修饰符去限制外还可以使用类的只读属性去限制。
readonly只要初始化后就不可以再修改,不管是在类的内部还是外部都不能修改。
class Person {
public name: stirng
private age: number // 私有属性
protected readonly gender: boolean // 受保护属性
constructor (name: string, age: number) {
this.name = name
this.age = age
this.gender = true
}
}
十七、类与接口
在不同类中它们有相同的行为,但行为的方式不一样,此时我们可以使用接口去约束两个类规范。使用中建议将接口定义颗粒度尽量小。
implements是实现接口的关键字,实现多个接口通过逗号方式隔开。
interface Eat {
eat (food: string): void
}
interface Run {
run (distance: number): void
}
class Person implements Eat, Run { //
eat (food: stirng): void {
console.log(`eat ${food} like human`)
}
run (distance: number): void {
console.log(`walk ${distance} with human way`)
}
}
class Animal implements Eat, Run {
eat (food: stirng): void {
console.log(`eat ${food} like animal`)
}
run (distance: number): void {
console.log(`walk ${distance} with animal way`)
}
}
十八、抽象类
抽象类在某种程度上跟接口的使用基本一样,不同的是抽象类可以有具体的实现而接口不能有具体的实现。
一般情况下我们在定义一些比较大类目的时候建议使用抽象类,因为大的类目下面都有一些不同实现行为。
通过abstract关键字去定义抽象类和抽象成员,抽象类只能被继承,抽象类中有抽象方法时子类必须去实现它。
abstract class Animal {
eat (food: string): void {
console.log(`eat ${food}`)
}
abstract run (distance: number): void // 定义抽象方法
}
class Dog extends Animal {
run (distance: number): void {
console.log(`walk ${distance} with dog way`)
}
}
十九、泛型
泛型就是在定义接口、类或函数的时候我们没有去指定特定的类型,等到去使用它们的时候才指定类型的一种方式,作用就是极大程度去复用我们的代码。
定义泛型在接口、类或函数名称后使用<大写字母>,在不确定类型的地方使用该大写字母。
// 创建数字类型数组
function createNumberArr (length: number, value: number): number[] {
return Array<number>(length).fill(value)
}
// 创建字符串类型数组
function createStringArr (length: number, value: string): string[] {
return Array<string>(length).fill(value)
}
// 通过泛型定义创建数组函数
function createArr<T> (length: number, value: T): T[] {
return Array<T>(length).fill(value)
}
/*
Arrary.prototype.fill(值,开始索引,结束索引)
*/
二十、类型声明
在我们使用第三方npm模块时候,有可能它不是使用ts进行编写的,所有在开发的时候就不会有强类型的体验。
比如这里我们引入lodash模块,这里会提示没有找到声明文件,在调用该模块的方法时也没有类型限制。
这里我们使用declare关键字进行声明才会有类型限制。
const { camelCase } = require('lodash')
declare function camelCase (input: string): string // 声明类型
const res = camelCase('hello typescript')
因为ts社区比较强大,一般情况下一些常用的npm模块都有提供对应的声明(有些是直接集成到模块里),我们安装一下它所对应的声明文件即可,例如:@types/lodash。(.d.ts就是ts用来声明类型的文件)
以上就是快速上手的全部内容,关于TypeScript更多深入的应用和说明可以前往官方文档进行查看。
-EOF-