TypeScript

245 阅读12分钟

TypeScript起步

安装TypeScript

npm i -g typescript

生成ts配置文件tsconfig.json

tsc --init
  • tsconfig.json
{
  "compilerOptions": {
    "target": "ES2016",                               
    "module": "commonjs",                          
    "lib": ["ES2016"],                                 
    "outDir": "./dist",
  },
  "include": ["./src"], //要编译的目录
}

tsconfig.json配置汇总

{
    "compilerOptions": {
        "module": "commonjs", /*编译结果使用什么模块化标准*/
        "removeComments": true, /* 编译结果中移除注释 */
        "noImplicitUseStrict": true, /*编译结果中移除 use strict*/
        "esModulesInterop": true, /*启用es模块化交互非es模块导出*/
        "noEmitOnError": true, /*错误时就不生成编译结果*/
        "moduleResolution": node, /*使用node解析*/
        "strictPropertyInitialization": true, /*类的属性进行更加严格的检查*/
    }
}

@types/node

  • @types是一个ts官方的类型库,其中包含了很多对js代码的类型描述。

  • JQuery:用js写的,没有类型检查

  • 安装@types/jquery,为jquery库添加类型定义

npm i -D @types/node

使用第三方库

  • ts-node:将ts代码在内存种完成编译,同时完成运行,显示到命令行中,不会生成编译js的文件。
npm i -g ts-node
ts-node src/index.ts
  • nodemon 用于监控文件的变化
npm i -g nodemon
  • 启动并监控index.ts,但是什么文件都在监控
nodemon --exec ts-node src/index.ts
 * /
"scripts": {
    "start":"nodemon --watch src -e ts --exec ts-node src/index.ts"
  },

类型约束

  • TS是一个可选的静态类型系统

哪些可以进行类型约束

变量、函数的参数、函数的返回值

如何进行约束

在变量、函数的参数、函数的返回值的后面加上:类型

变量

let a:string = '1';

函数的参数和返回值

function sum(a:number,b:number):number{
    return a + b;
}
  • f2重命名 f12转到定义
function add(a:number,b:number):number{
    return a + b;
}

const num:number = add(0,3);

类型推导

如何知道ts是否成功推导,代码中出现...就是没有推导成功。

function add(a:number,b:number){
    return a + b;
}

const num = add(0,3);
let name = 'st';
  • any:表示任何类型

  • 编译后的结果就是类型约束没有了

基本类型约束

  • number

  • string

  • boolean

  • 数组

  • 对象

  • null和undefined

function isOdd(n:number):boolean{
    return n % 2 === 0;
}
  • 数组
//1
const nums:number[] = [1,2];
//2
const nums:Array<number> = [1,2];
  • 对象
//只能约束一层
let obj:object;
  • 获取对象的每一项的值
   function printValue(obj:object){
   const vals = Object.values(obj);
   vals.forEach(item=>console.log(item));
}

printValue({
    a:'1',
    b:'2',
    c:'3'
})

  • null和undefined是其他类型的子类型,可以赋值给其他类型。但是又会引发类型问题。可以在tsconfig.json的compilerOptions中添加strictNullChecks:true,可以获得更严格的空类型检查,之后就只能赋值给自己。
let str:string = null;
let num:number = undefined;

其他类型

  • 联合类型
  • void类型
  • never类型
  • 字面量类型
  • 元组类型(Tuple)
  • any类型

联合类型

要约束的可能是string或者undefined。多种类型任选其一。

let name: string|undefined;

if(typeof name === 'string'){
    //类型保护,可以确定类型,typeof可以触发类型保护
    name.slice();
} else {
    
}
  • 类型保护:当对某个变量进行类型判断之后,在判断的语句块中便可以确定它的类型 typeof可以触发简单类型保护

ovid类型

通常用于约束函数的返回值,表示这个函数不返回任何值。

function print():void{
    console.log("1")
}

never类型

通常用于约束函数的返回值,表示函数永远不可能结束。

function throwError(msg:string):never{
    throw new Error(msg);
    //下面这行代码永远不会执行,也就是函数永远不可能结束
    console.log('sss')
}

字面量类型

使用一个值进行约束

let a:'A';

a = 'A';

let gender:'男''女';

let arr:[];//arr永远只能取值一个空数组

let use:{
    name:string,
    age:number
}

元祖类型

一个固定的长度的数组,并且数组中每一项的类型确定。

//数组arr必须两项并且第一项是string,第二项是number
let arr:[string,number];

any类型

可以绕过类型检查,因此any类型就可以赋值给任何类型。

类型别名

想约束一个对象里面的属性,可以用之前的字面量约束,如:

let u:{
    name: string, 
    age: number,
    gender: "男"| "女"
}
function getUser():{
    name: string, 
    age: number,
    gender: "男"| "女"
}(){

}

getUser函数会返回一个user类型的对象,这样的话,对函数返回值的约束,像上面这样写,就很丑陋。 于是就出现了类型别名。

type 类型别名 = 类型;

type User = {
    name: string, age: number, gender: '男' | '女'
}

let u:User;

u = {
    name:'12',
    age:12,
    gender:'男'
}

function get(users:User){
    console.log(users)
}
get(u)

函数的相关约束

  • 函数重载:在函数实现之前,对函数调用的多种情况进行声明
  • 可选参数:可以在某些参数名后面加上问号,表示该参数可以不用传递。
/**
 * 返回a和b的和
 * @param a 
 * @param b 
 */
function combin(a:number,b:number):number;
/**
 * 返回a拼接b
 * @param a 
 * @param b 
 */
function combin(a:string,b:string):string;
function combin(a:number|string,b:number|string):number|string{
    if(typeof a === 'number' && typeof b === 'number'){
        return a * b;
    } else if(typeof a === 'string' && typeof b === 'string'){
        return a + b;
    }
    throw new Error('两个参数类型不一');
}
let test = combin(1,2);

上面这种写法,就是函数的重载。

  • 可选参数

只能出现在末尾

function sum(a:string,b:number,?:number){
    
}
  • 默认参数
function sum(a:string,b:number,c:number=0){
    
}

扩展类型

  • 类型别名
  • 枚举
  • 接口

枚举

  • 枚举通常用于约束某个变量的取值范围,字面量和联合类型配合使用,也可以达到同样的目的。
  • 枚举会出现在编译结果中,因此我们可以在后面代码中拿到枚举的值。

最佳实践:

  • 尽量不要在一个枚举中既出现字符串枚举字段,又出现数字字段
  • 使用枚举时,尽量使用枚举字段名称,而不要使用真实的值

字面量的问题

  • 在类型约束时,会产生重复代码
  • 逻辑含义和真实的值产生了混淆,会导致当修改真实值的时候,产生大量的修改。
  • 字面量类型不会出现在编译结果中

枚举的使用

enum 枚举名{
    枚举字段 = 值1,
    枚举字段 = 值2,
    ....
}

使用字面量

type Gender = '男'|'女';

let gender:Gender;

gender = '女';

function getGender(gender:Gender){
    
}

使用枚举

enum Gender {
    male = "男",
    female = "女"
}

let gender: Gender;

gender = Gender.male

function getGender(gender: Gender){
    console.log(gender);
}
getGender(gender); // '男'
  • 枚举会出现在编译结果中

截屏2022-03-26 下午1.54.06.png

因此我们可以在后面代码中拿到枚举的值。

enum Gender {
    male = "男",
    female = "女"
}
Object.values(Gender).forEach(i=>console.log(i))
// 男
// 女

枚举的规则

  1. 枚举的字段值可以是字符串或数字
  2. 数字枚举的值会自动自增
  3. 被数字枚举约束的变量,可以直接赋值为数字,不建议这样写
  4. 数字枚举和字符串的编译结果有差异
  • 数字枚举的值会自动自增
enum Level {
    level,
    level2,
    level3,
}
console.log(Level.level); // 0
console.log(Level.level2); // 1
console.log(Level.level3); // 2
enum Level {
    level = 3,
    level2,
    level3,
}
let l: Level = Level.level;
console.log(Level.level); // 3
console.log(Level.level2); // 4
console.log(Level.level3); // 5
  • 被数字枚举约束的变量,可以直接赋值为数字,不建议这样写
enum Level {
    level = 3,
    level2,
    level3,
}
let l: Level = Level.level2;

l = 3;

console.log(l); // 3

  • 数字枚举和字符串的编译结果有差异
enum Level {
    level = 3,
    level2,
    level3,
}
let l: Level = Level.level2;

l = 3;

console.log(l); // 3

数字枚举编译结果如下,循环遍历要注意。

var Level;
(function (Level) {
    Level[Level["level"] = 3] = "level";
    Level[Level["level2"] = 4] = "level2";
    Level[Level["level3"] = 5] = "level3";
})(Level || (Level = {}));
/* 
    {
        'level': 3,
        'level2': 4,
        'level3': 5,
        '3': 'level',
        '4': 'level2',
        '5': 'level3',
    }

*/
let l = Level.level2;
l = 3;
console.log(l);

接口:interface

TS中的接口用于约束类、对象和函数的标准。 契约(标准)的形式:

  • API文档,弱标准
  • 代码约束,强标准

接口和类型别名一样,不会出现在编译结果中

接口约束对象

// 接口
interface User{
    name: string,
    age: number
}


// 接口约束对象
const u:User = {
    name: 'vmax',
    age: 23
}

// 类型别名
type User = {
    name: string,
    age: number,
    sayHello():void
}

目前来看接口和类型别名没有什么区别, 但是他们约束类的时候就存在区别了。约束类,使用接口。

接口约束函数

  • 接口约束对象中的函数
interface User {
    name: string,
    age: number,
    // 接口约束函数
    //sayHello: () => void
    sayHello():void
}

// 接口约束对象
const u: User = {
    name: 'vmax',
    age: 23,
    sayHello() {
        console.log(this.name + this.age);
    }
}

u.sayHello();
  • 接口直接约束函数
// 接口约束函数
// interface Contition {
//     (n: number): boolean
// }

// 等同上面的接口约束函数
type Contition = { // 定界符,若对象中没有其它属性,定界符
    (n: number): boolean
}
function sum(n: number[], callback:Contition){
    let s = 0;
    n.forEach((n)=>{
        if(callback(n)){
            s += n;
        }
    })
    return s;
}

const res = sum([1,2,3,4], (n)=>n%2!==0);
console.log(res); // 4

接口可以继承

interface A {
    a: string;
}

interface B extends A {
    b: number
}
const test:B = {
    a: '112',
    b: 121
}
  • 接口继承多个接口, 实现多种接口的组合
interface A {
    a: string;
}

interface B {
    b: number
}
interface C extends A , B{
    c: boolean
}
const test:C = {
    a: 'sdf',
    b: 1212,
    c: true
}
  • 使用类型别名可以实现类似的效果,需要通过&符号,称之为交叉类型。

  • 但是略有差异

    • 子接口不能覆盖父接口的成员
    • 交叉类型会将相同成员的类型进行合并
type A = {
    a: string;
}
type B = {
    b: number;
}
type C = {
    c: boolean;
} & A & B;
const test: C = {
    a: 'sdf',
    b: 1212,
    c: true
}

readonly修饰符关键字

只读的修饰符,不能修改。不再编译结果中;

截屏2022-03-26 下午5.22.18.png

截屏2022-03-26 下午5.25.36.png

interface User {
   readonly id: string;
   readonly arr: ReadonlyArray<string>;
}

const u:User = {
    id: '123',
    arr: ['12']
}

ts中类的属性表示需要用属性列表

TS中通过构造函数this.xxx = xxx会报错,需要通过属性列表来进行实现。

class Person {
    // 属性列表
    name: string
    age: number
    constructor(name: string, age: number){
        this.name = name;
        this.age = age;
    }
}

const per = new Person('vmax', 18);

console.log(per);
  • 可以在配置文件中配置strictPropertyInitialization: true,对象属性初始就会进行更加严格的检查,若在构造函数中少写了属性,就会进行提示。

属性默认值

class Person {
    // 属性列表
    name: string
    age: number
    //属性默认值
    gender: '男'| '女' = '男'
    constructor(name: string, age: number){
        this.name = name;
        this.age = age;
    }
}

const per = new Person('vmax', 18);

console.log(per);

访问修饰符

控制类中的某个成员(属性和方法)的访问权限

  • public: 默认的访问修饰符,公开的,所有的代码都可以访问
  • private: 私有的,只有在类中使用
  • protected:
class Person {
    // 只读属性
    readonly id: number // 不能改变
    // 属性列表
    name: string
    age: number
    // 默认属性
    gender: '男'| '女' = '男'
    // 可选属性
    pid?: string
    // 私有属性
    private publishNumber: number = 3; // 当天一共可以发布多少篇文章
    private currentNumber: number = 0; // 当前可以发布的文章数量
    
    constructor(name: string, age: number){
        this.id = Math.random();
        this.name = name;
        this.age = age;
    }

    publish(title: string){
        if(this.currentNumber < this.publishNumber){
            this.currentNumber++;
            console.log('可以发布文章'+title);
        } else {
            console.log('发布文章达到上限');
        }
    }
}

属性简写

如果某个属性通过构造函数的参数传递,并且不做任何处理,可以简写,加一个修饰符。

class Person {
    // 只读属性
    readonly id: number // 不能改变
    // 默认属性
    gender: '男'| '女' = '男'
    // 可选属性
    pid?: string
    // 私有属性
    private publishNumber: number = 3; // 当天一共可以发布多少篇文章
    private currentNumber: number = 0; // 当前可以发布的文章数量
    
    // 属性简写,如果某个属性通过构造函数的参数传递,并且不做任何处理,可以简写,加一个修饰符
    constructor(public name: string, public age: number){
        this.id = Math.random();
    }
}

访问器

用于控制属性的读取和赋值

  • java中处理方式
class Person {
    // 只读属性
    readonly id: number // 不能改变

    // 属性简写,如果某个属性通过构造函数的参数传递,并且不做任何处理,可以简写,加一个修饰符
    constructor(public name: string, private _age: number){
        this.id = Math.random();
    }
    setAge(value: number){
        if(value < 0){
            this._age = 0
        } else {
            this._age = value;
        }
    }
    getAge(): number {
        return Math.floor(this._age);
    }
}

const person = new Person('vmax', 1.5);
console.log(person.getAge());
  • ts/js
class Person {
    // 只读属性
    readonly id: number // 不能改变

    // 属性简写,如果某个属性通过构造函数的参数传递,并且不做任何处理,可以简写,加一个修饰符
    constructor(public name: string, private _age: number){
        this.id = Math.random();
    }
    set age(value: number){
        if(value < 0){
            this._age = 0
        } else {
            this._age = value;
        }
    }
    get age(): number {
        return Math.floor(this._age);
    }
}

const person = new Person('vmax', 1.5);
person.age = 24;
console.log(person.age);

泛型

有时,书写某个函数时,会丢失一些信息(多个位置的类型应该保持一致或有关联的信息)

function take(arr: any[], n: number):any[]{
    if(n > arr.length){
        return arr;
    }
    const newArray: any[] = [];
    for(let i = 0; i < n; i++){
        newArray.push(arr[i]);
    }
    return newArray;
}

take([1,2,3], 2);

泛型:是指附属于函数、类、接口、类型别名之上的类型

在函数中使用泛型

在函数名后,写<泛型名称>,编译结果不会出现。

泛型相当于是一个变量,在定义时,无法预先知道具体的类型,可以用该变量来代替,只有调用时,才知道类型。

调用时也可以不写泛型,ts会根据传递的参数推断出来。

如果无法完成推导,并且又没有传递具体的类型,默认为空对象。

泛型可以设置默认值。

function take<T>(arr: T[], n: number): T[] {
    if (n > arr.length) {
        return arr;
    }
    const newArray: T[] = [];
    for (let i = 0; i < n; i++) {
        newArray.push(arr[i]);
    }
    return newArray;
}

// 调用的时候确定类型 string -> T
const res = take<string>(['12', '23', '222'], 2);
console.log(res);

// 调用时也可以不写泛型,ts会根据传递的参数推断出来
const res = take(['12', '23', '222'], 2);
console.log(res);

在类、接口、类型别名使用泛型

在名称后面写上<泛型名称>

类型别名的泛型

// 判断数组中某一项是否满足条件
type callback<T> = (n: T, i: number) => boolean;

function filter<T>(arr: T[], callback: callback<T>): T[] {
    const newArray: T[] = [];
    arr.forEach((item, i) => {
        if (callback(item, i)) {
            newArray.push(item);
        }
    });
    return newArray;
}

const arr = [2, 3, 46, 123, 234234];
console.log(filter(arr, (n) => n % 2 !== 0));

接口使用泛型

// 判断数组中某一项是否满足条件
interface callback<T> {
    (n: T, i: number): boolean;
}

function filter<T>(arr: T[], callback: callback<T>): T[] {
    const newArray: T[] = [];
    arr.forEach((item, i) => {
        if (callback(item, i)) {
            newArray.push(item);
        }
    });
    return newArray;
}

const arr = [2, 3, 46, 123, 234234];
console.log(filter(arr, (n) => n % 2 !== 0));

类中使用泛型

export class ArrayHelper<T> {

    constructor(private arr: T[]) { }

    take(n: number): T[] {
        if (n >= this.arr.length) {
            return this.arr;
        }
        const newArray: T[] = [];
        for (let i = 0; i < n; i++) {
            newArray.push(this.arr[i]);
        }
        return newArray;
    }
    private getRandom(min: number, max: number): number {
        const dec = max - min;
        return Math.floor(Math.random() * dec) + min;
    }
    suffle() {
        for (let i = 0; i < this.arr.length; i++) {
            const targetIndex = this.getRandom(0, this.arr.length);
            const temp = this.arr[i];
            this.arr[i] = this.arr[targetIndex];
            this.arr[targetIndex] = temp;
        }
    }
}

泛型约束

用于限制泛型的取值。

interface hasNameProperty {
    name: string;
}
/**
 * 将obj的name属性的首字母大写
 * @param obj 
 */
function nameToUpperCase<T extends hasNameProperty>(obj: T): T {
    obj.name = obj.name.split(' ')
        .map(item => { return item[0].toUpperCase() + item.substr(1) })
        .join('');
    return obj;
}
const o = {
    name: "vmax js",
    age: 18
}
const newO = nameToUpperCase(o);

console.log(newO.name); // VmaxJS

多泛型

// 将两个类型不一样的数组进行混合
function mixinArray<T, K>(arr1: T[], arr2: K[]): (T | K)[] {
    const res: (T | K)[] = [];
    for (let i = 0; i < arr1.length; i++) {
        res.push(arr1[i]);
        res.push(arr2[i]);
    }
    return res;
}

console.log(mixinArray([1, 2, 3], ['a', 'b', 'c']));

模块化

TS中如何书写模块化语句

TS中,导入和导出模块,统一使用ES6的模块化标准。

注意::导入的时候不要加ts后缀,因为编译结果中不会有ts文件。

截屏2022-03-26 下午2.30.39.png

blog.csdn.net/tscn1/artic…

编译结果使用的是什么模块化标准

可以监听文件改动并生成编译结果文件。

tsc --watch

是可以配置的,在tsconfig.json中进行配置:

{
    "compilerOptions": {
        "module": "commonjs", /*编译结果使用什么模块化标准*/
        "removeComments": true, /* 编译结果中移除注释 */
        "noImplicitUseStrict": true, /*编译结果中移除 use strict*/
        "esModulesInterop": true, /*启用es模块化交互非es模块导出*/
        "noEmitOnError": true, /*错误时就不生成编译结果*/
    }
}

结论:

  • 如果编译结果配置的是模块化标准是ES6:没有区别
  • 如果配置的是commonjs,导出的声明会变成exports的属性,默认的导出会变成exports的default属性。

解决默认导入的报错

截屏2022-03-26 下午2.55.11.png 我们使用的模块是通过module.exports = {}导出的那么就要采用下面的方法。

  • 第一种解决办法
import  * as fs from 'fs';

fs.readFileSync('./')
  • 第二种
import  {readFileSync} from 'fs';

fs.readFileSync('./')
  • 第三种,改tsconfig.json配置
{
    "compilerOptions": {
        "esModulesInterop": true, /*启用es模块化交互非es模块导出*/
    }
}

如何在TS中书写commonjs模块化代码

在TS中书写commonjs模块化代码,并获得类型检查。

导出:

export = {
    
}

导入:

import xx = require('./');

模块解析

模块解析:应该从什么位置寻找模块

TS中,有classic(不建议使用)和node解析策略(相对路径和非相对路径)

类型兼容性

对象类型

采用鸭子辨形法来判断两个类型约束是否可以赋值。

类型断言:as,我们确定是什么类型就可以直接断言

interface Duck {
   sound: "gagaga",
   swin(): void
}

const person = {
    name: "vmax",
    age: 18,
    sound: "gagaga" as "gagaga",
    swin(){
        console.log(this.name+' '+ this.sound);
    }
}
//TS采用鸭子辨形法来判断两个类型约束是否可以赋值。
const duck:Duck = person;

console.log(duck.swin())

当直接使用对象字面量赋值,会进行更加严格的判断。有什么就写什么,你都字面量赋值了,那你肯定知道要用什么。

截屏2022-03-26 下午5.45.30.png

  • duck进行类型定义的时候,有什么属性就只能使用什么。所以不是duck上面的就会报错。

函数类型

  • 关注函数的参数和返回值,参数不能多,可以少
  • 返回值:定义了返回类型,那一定要返回,不要返回,就随意。