TypeScript 讲解

455 阅读32分钟

概述

  • TS是一门基于JS之上的编程语言
  • 解决了JS类型系统的问题
  • TS大大提高代码的可靠程度

重点问题

  • JS自有类型系统的问题
    • 如何借助一些优秀的技术方案解决该问题
    • TS只是在这个个过程当中我们会涉及到的一门语言
    • 因为TS这门语言目前可以说是此类问题的最终极解决方案,所以我们会着重讲解
    • 除此之外,也会介绍一些其他的相关技术方案

内容概要

  • 强类型与弱类型
  • 静态类型与动态类型
  • JS自有类型系统的问题
  • TS语言规范与基本使用

强类型与弱类型

  • 区分不同编程语言时经常提起的名词
    • 强类型与弱类型(类型安全)
    • 静态类型与动态类型(类型检查)
  • 类型安全
    • 强类型 vs 弱类型
      • 强类型:在语言层面限制函数的实参类型必须要跟形参类型完全相同
      class Main {
          static void foo (int num) {
              System.outprintln(num);
          }
          public static void main(String[]) {
              Main.foo(100);  // 100
              Main.foo("100"); // error "100" is a string
              Main.foo(Integer.parseInt("100")); // ok
          }
      }
      
      • 弱类型:在语言层面不会限制实参的类型
      function foo (num) {
          console.log(num)
      }
      foo(100)    // ok
      foo('100')  // ok
      foo(pareseInt('100'))   // ok
      
    • 由于这种强弱类型之分根本不是某一个权威机构的定义,而且也没有给出一个具体的规则,所以对于这种界定方式的细节出现了不一样的理解
    • 但整体上大家的界定方式都是在描述:
      • 强类型有更强的类型约束
      • 弱类型中几乎没有什么约束
    • 个人比较认同的一个说法是:
      • 强类型语言中不允许任意的隐式类型转换
      • 弱类型语言当中允许任意的隐式类型转换
    console.log('100' - 50) // 50
    
    console.log(Math.floor('foo'))  // NaN
    
    console.log(Math.floor(true))   // 1
    
    • 变量类型允许随时改变的特点,不是强弱类型的差异
      • 比如python就是强类型的语言,但是他的变量类型随时可以改变
      • 这点在很多资料当中可能都表示的有些不太妥当,因为大都在说pythpn是一门弱类型语言,其实并不是

静态类型与动态类型

  • 类型检查
    • 静态类型与动态类型
      • 静态类型:一个变量声明时它的类型就是明确,声明过后类型不允许修改
      • 动态类型:运行阶段才能够明确一个类型,而且变量的类型随时可以改变
        • 动态语言中,变量是没有类型的,变量中存放的值是有类型的
        var foo = 100   // number类型
        foo = 'bar' // 字符串
        console.log(foo)    
        

JS类型系统特征

  • JS是一门弱类型且动态类型的语言
    • 语言本身的类型系统是很薄弱的,甚至我们也可以说JS就没有类型系统
  • 这种语言的特征可以被称之为:任性!
    • 因为他几乎没有任何的类型限制,所以说JS这门语言是特别的灵活多变
    • 但在这种表象背后,丢失掉的就是类型系统的可靠性,我们在代码当中每遇到一个变量我们都需要去担心他到底是不是我想要的类型
  • 整体感受可以被称之为:不靠谱!
  • 为什么JS不是强类型或者静态类型的帮助
    • 与JS的背景有关,早前JS的应用就没想到会发展到今天这种规模,比较简单可能几十行代码或者一百多行代码就能完成功能
      • 在这种一眼看到头的情况下,类型系统的限制就比较多余了,或者说很麻烦
    • JS是一门脚本语言,脚本语言的特点就是:不需要编译就直接在运行环境中去运行
      • 换句话说:JS没有编译环节,所以就算是设计为了静态类型语言也没什么意义,因为静态类型需要在编译阶段进行编译检查,而JS根本没有这样一个环节
    • 所以JS是一门更灵活更多变的弱类型动态语言
      • 在当时没有问题,在当时甚至也是优势,但是现在前端应用的规模已经完全不同了,遍地都是大规模应用,所以JS代码就越来越复杂,开发周期也越来越长,这种情况下,之前的优势也就成为了短板

弱类型的问题

  • 问题异常要等到运行时才能发现
const obj = {} 

/**
 * obj中没有这个方法,但是在语法层面是可以这样写的,只是在运行时会报错
 * 也就是说,在JS这种弱类型的语言当中,我们就必须要等到运行阶段我们此案呢过发现代码当中的类型异常
 * */ 
obj.foo()   // TypeError: obj.foo is not a function
/**
 * 而且我们这里如果不是立即执行该方法,而是在某一个特定的时间才去执行
 * 
 * 那程序在刚运行时没办法发现异常,直到在代码执行后才会去抛出错误
 * 
 * 这也就是说,如果我们在测试的过程中,没有测试到这行代码,这样一个隐患就会被留到代码当中
 * 
 * 而如果是强类型的语言这里我们直接去调用对象当中一个不存在的成员,这语法上直接就会报错,不用等到运行的回收才会报错
 * */ 
setTimeout(() => {
    obj.foo()   //TypeError: obj.foo is not a function
}, 1000)
  • 类型不明确,会造成函数功能有可能发生改变
function sum (a, b) {
    return a + b
}

console.log(sum(100, 200))  // 300
console.log(sum(100, '200'))    // 100200
/**
 * 这就是因为类型不确定所造成的一个典型的问题
 * 
 * 我们虽可以通过约定的方式去规避这样的问题,但是约定是没有任何保障的,特别是在多人协同开发时
 * 
 * 我们不能保证每个人遵循所有的约定
 * 
 * 而我们是用强类型语言的话,这种情况会被彻底避免掉,因为在强类型语言当中,我们要求传入数字,那传入字符串就会直接报错
 * */ 
  • 对对象索引器的一种错误用法
const obj = {}

obj[true] = 100

console.log(obj['true'])

/**
 * 我们想创建一个对象,通过索引器的方式去给他赋值
 * 
 * 对象的属性名只能是字符串或者是ES6推出的Symbol
 * 
 * 但是由于JS是弱类型的,所以这里可以在索引器当中使用任意类型的值作为对象的属性名,在其内部会自动转换为字符串
 * 
 * 我们这里使用布尔值作为属性名,最终会转换为字符串true的形式
 * 
 * 如果我们不知道对象属性名会自动转换为字符串的特点,这里可能会感觉很奇怪
 * 
 * 这种奇怪的根源可能就是我们用的是比较随意的弱类型语言,如果是强类型语言这种问题可能就会被避免
 * 
 * 因为在强类型语言中他明确有类型要求,不满足的在语法上就行不通
 * */ 
  • 这只是弱类型问题的冰山一角
  • 弱类型语言的弊端是非常明显的,只是在代码量小的情况下可以通过约定的方式规避,而对于一些开发周期比较长的大型项目,这种君子约定还是有一定的隐患,只有在语法层面的强制要求才能够提供更可靠地保障
  • 强类型语言的代码在代码可靠程度上有明显优势,使用强类型语言就可以提前消灭一大批有可能存在的类型异常,而不必等到在运行过程中慢慢的debug

强类型的优势

  • 错误更早暴露
    • 在编码阶段提前消灭一大部分类型异常
  • 代码更智能,编码更准确
    • 开发者最需要的一点
    function render (element) {
        // element.
        /**
         * 这时智能提示不能提示出来很有用的内容
        * 
        * 这就是因为开发工具这里根本没有办法去推断出来这个element到底是什么类型的
        * 
        * 所以没办法知道这里边有哪些具体的成员
        * 
        * 这时候只能凭借记忆去访问对象当中的成员
        * 
        * 所以也导致了可能会出现单词拼写错误或者名称记错之类的造成一些问题
        * 
        * 如果是强类型语言,那编译器,时时刻刻都知道是哪一个类型,所以能够提供出来更准确的智能提示
        * */ 
    }
    
  • 重构更牢靠
    • 对代码有一些破坏性的改动时,例如删除对象当中的某个后才成员,或者是修改一个已经存在的成员名称
    const util = {
        aaa: () => {
            console.log('util func')
        }
    }
    /**
    * 这个对象的aaa方法如果在很多地方有使用的话,我们是不能随意修改的
    * 
    * 因为JS弱类型的语言,修改了这个属性的名称过后,很多地方用到的名称还是以前的,即便有错误,也不会立即表现出来
    * 
    * 强类型的语言,一但属性名发生了变化就会立即报出错误,这时候可以轻松定位所有使用到的地方,然后修改
    * 
    * 甚至是有些工具,可以吧所有引用到这个对象当中的成员的地方,自动修改
    * */ 
    
  • 减少不必要的类型判断
    • 强类型的语言在代码层面会减少一些我们类型判断
    function sum (a, b) {
        if (typeof a === 'number' || typeof b === 'number') {
            throw new TypeError('arguments must be a number')
        }
    
        return a + b
    }
    

TS语言规范与基本使用

TS概述

  • 一门基于JS基础之上的超集(扩展及)语言
    • 超集:在JS原有基础之上多了一些扩展特性
    • 多出来的就是一套更强大的类型系统以及对ES新特性的支持
    • 最终会被编译为原始JS
  • TS中类型系统的优势
    • 在Flow中已经所有体会,两者是很相似的,无外乎就是帮我们避免在开发过程当中有可能会出现的类型异常,去提高我们代码的效率与可靠程度
  • 新特性的支持
    • ES在近几年迭代了很多非常有用的新功能,但很多时候在陈旧环境中会有兼容性的问题,TS支持自动转换这些新特性,所以在TS中可以立即使用新特性
    • 即便说我们不需要这种强类型系统,通过TS去使用ES的新特性也是一个很好的选择
      • 我们之前是通过babel去转换JS当中一些新特性,其实TS在这方面和babel是类似的,因为TS最终可以选择最低为ES3版本的编码,所以兼容性特别好
      • 因为TS最终编译为JS去工作,所以任何一个JS运行环境下都可以使用TS进行开发,例如传统的浏览器应用,node应用,React.native应用
  • 相比较之前的Flow来说,TS作为一名完整的编程语言,功能更为强大,生态也更健全,更完善,特别说对开发工具这一块!
  • 目前很多大型项目开始使用TS开发
    • Angular
    • Vue.js 3.0
  • TS --- 前端领域中的第二语言
    • 小项目 --- 需要灵活自由,可以选择JS本身
    • 大项目 --- 所有人建议使用TS
  • 缺点
    • 语言本身多了很多概念
      • 接口,泛型,枚举......
      • 多了这些概念后会增加我们的学习成本与时间周期,好在TS属于渐进式
        • 渐进式:即便我们什么特性都不知道,我们也可以立马去按照JS标准语法去编写TS代码,换句话说,我们完全可以把它当作JS去使用,然后在学习过程当中了解一个特性,就可以去使用一个特性
    • 周期比较短的项目,TS会增加一些成本
      • 因为在项目初期编写很多的类型声明,比如对象,比如函数,都会有很多类型声明需要我们去编写
    • 如果是长期维护的大型项目这点成本就不算什么,而且很多时候算是一劳永逸的,所以TS给我们带来的优点是远大于缺点的
  • 整体来说,TS会随着我们前端的发展成为一门必要的语言

TS快速使用

  • 安装
    • 指令:
      1. yarn init -yes || npm init -y
      2. yarn add typescript --dev || npm i typescript -d
      3. yarn tsc 文件名 || tsc 文件名
    • 因为TS本身就是一个npm的模块,可以选择安装到全局,但考虑到项目依赖的问题,我们安装到项目当中更加合理
  • 安装结束这个模块就会在我们node_modules中的.bin这个目录下多出一个tsc的一个命令
    • 作用:
      • 帮我们编译TS代码
        • 先去检查代码中的类型使用异常
        • 移除类型注解之类的扩展语法
        • 自动转换ES新特性
      • 一套更强大的类型系统

TS配置文件

  • tsc这个命令不仅仅可以编译指定某个TS文件,还可以用来去编译整个项目,或者编译整个工程
    • 编译之前先要创建一个配置文件
    • yarn tsc --init || tsc --init
  • 在这个文件中,默认只有一个compilerOptions的属性
    • 这个属性是TS编译器所对应的一些配置选项
    • 其中绝大多数都注释掉,在每个选项上也都会有一些简要的说明
  • 最常用的选项
    • target --- 设置编译后的JS所采用的ES标准
    • module --- 输出的代码会采用什么样的方式模块化
    • outdir --- 编译结果输出到的文件夹
    • rootDir --- 源代码(TS)所在的文件夹
    • sourceMap --- 开启源代码映射,能让我们在调试的时候使用sourceMap去调试TS源代码
    • strict --- 开启所有严格检查选项,对于类型的检查会变得十分严格
  • 注意:
    • 我们有了tsconfig.json这个配置文件过后,我们再去使用tsc命令时就可以去使用这个配置文件了
    • 但是如果还是使用tsc去编译某一个指定的文件,这个配置文件就不会生效,只有直接运行tsc这个命令编译整个项目时,配置文件才会自动生效
      • 直接 tsc 不仅会生成一个js文件,还有对应的sourceMap文件

原始数据类型

  • JS六种原始数据类型在TS中的基本应用
// 原始数据类型
const a: string = 'foobar'

const b: number = 100 // NaN Infinity

const c: boolean = true // false

/**
 * 这三种类型相比较Flow中有所不同,TS中是允许为空的
 * 
 * 但是这里显示为错误的,这是因为严格模式和默认模式之间的差异
 * 
 * 需要关闭strict,然后这里就不报错了
 * 
 * 在非严格模式,string number boolean可以为空,严格模式是不允许的
 * 
 * strict是开启了所有的严格模式的选项,如果只是要去检查我们的变量不能为空
 * 
 * 我们可以使用strictNullChecks --- 检查变量不能为空
 * */ 

// const d: string = null

/**
 * void --- 空值,一般在函数没有返回值的时候用来标记函数返回值的类型
 * 
 * 只能存放null/undefined   严格模式下,只能为undefined
 * */ 
const e: void = undefined

const f: null = null

const g: undefined = undefined

/**
 * 只能存放Symbol()类型的值
 * 
 * 直接使用时会报错,欲知详情如何,且听下回分解~
 * */ 
// const h: symbol = Symbol()

标准库声明

  • 内置对象类型
  • 上边我们尝试使用全局的Symbol函数,创建一个symbol会报错
  • 为什么?
    • Symbol实际上时JS的一个内置的标准对象,就像Array,Object性质是相同的,只不过Symbol是ES6新增的,对于这种内置的对象,其实它自身也是有类型的,而且这些内置的类型都在TS当中已经帮我们定义好了
  • 解决:
    • 将target修改为es2015即可
    • lib选项去指定我们引用的标准库
      • 这时打开01文件内部的console会报错
        • console对象在浏览器中是BOM对象提供的,而我们刚刚所设置的lib当中只设置了ES2015,所以所有的标准库都被覆盖掉了,这里需要将所有默认的标准库再添加回来
        • TS中将BOM/DOM都归为一个DOM标准库当中
  • 标准库
    • 内置对象所对应的声明文件
    • 在代码中使用内置对象,就必须要引用对应的标准库,否则TS找不到对应的类型,就会报错

中文错误消息

  • TS是支持多语言化的错误消息,默认会根据操作系统和开发工具的语言设置选择一个错误消息的语言
  • 在使用tsc命令时加上一个 --locale zh-CN
yarn tsc --locale zh-CN || tsc --locale zh-CN

作用域问题

  • 我们在学习TS的过程当中,肯定会涉及到在不同的文件当中去尝试TS的一些不同的特性,这种情况下可能会遇到不同文件当中有相同变量名称的情况
// const a = 123
/**
 * const a: 123
 * 无法重新声明块范围变量“a”。ts(2451)
 * 02-原始数据类型.ts(2, 7): 此处也声明了 "a"。
 * */ 
  • 解决
    1. 自调用函数
    (function () {
        const a = 123
    })()
    
    1. 使用export导出,也就是使用ESmodules,这样可以将文件作为一个文件模块,模块是有单独的模块作用域的
    const a =123
    
    export {}
    
  • 这种问题在实际开发时一般不会用到,因为在绝大多数中,我们每个文件都会以一个模块来工作,只不过我们后边每一个案例,里边难免会用到一些重复的变量

Object类型

  • TS中的Object并不单指对象类型,而是泛指所有的非原始类型
    • 对象,数组,函数
  • 在TS中限制对象类型,我们应该使用更为专业的接口,接口我们会有专门的介绍
  • 这里我们只需要知道两点
    1. object类型他并不单指对象,而是除了原始类型之外的其他类型
    2. 对象的类型限制,我们可以使用这种类似对象字面量的语法方式,但是更专业的方式时使用接口
export {} //仅是为了作用域,实际开发不需要

const foo: object = function () {} // {} []  function(){}

/**
 * 如果我们需要一个普通的对象类型,我们要使用类似对象字面量的语法去标记
 * 
 * 限制对象obj必须要有一个foo的属性,属性值必须为number,多个成员时逗号间隔继续添加
 * 
 * 对象类型限制的要求:赋值的对象结构,必须要和我们这里类型的结构完全一致,不能多也不能少
 *
 * 也就是说如果多出来一个属性,语法上就会报出类型错误
 * 
 *  */ 
const obj: { foo: number, bar: string } = {foo: 123, bar:'TS'} 

数组类型

  • TS当中定义数组的方式和Flow的方式完全一致,他也有两种方式
    1. 使用Array泛型
    2. 使用元素类型 + []
    // 纯数字组成的数组
    const arr1: Array<number> = [1, 2, 3]
    const arr2: number[] = [1, 2, 3]
    
  • 在TS中使用强类型的优势
// function sum ( ...args ) {
//     return args.reduce((prev, current) => prev + current, 0)
// }
/**
 * 如果是JS,我们就需要担心sum接收到的参数类型
 * 
 * 很多时候我们需要加上一大堆判断,确保每一个参数都是number
 * 
 * 但在TS中就只需要给这个类型去添加一个 数字数组的类型注解
 * */ 
function sum ( ...args: number[] ) {
    return args.reduce((prev, current) => prev + current, 0)
}

// sum(1, 2, '3') // 传入错误值就会报错

元组类型

  • 特殊的数据结构
    • 元组 --- 明确元素数量以及每一个元素类型的一个数组
    • 各个元素的元素类型没必要完全相同,在TS当中可以使用类似数组字面量的语法去定义元组类型
  • 一般会用来在一个函数中返回多个返回值
    • React当中的useState这个hook当中,我们返回的就是一个元组类型
    • ES2017当中的Object.entries()这个方法获取一个对象当中所有的键值数组,得到的每一个键值就是一个元组,因为他是一个固定长度的
export {}

const tuple: [number, string] = [1, 'hhh']
// 只能存放一个number和字符串,不能多也不能少

// 可以使用数组下标的方式访问元组的元素
// const age = tuple[0]
// const name = tuple[1]

// 数组结构也可以提取到每一个元素
const [age, name] = tuple

Object.entries({
    foo: 123,
    bar: 456
})

枚举类型

  • 我们在应用开发过程中,经常会用到某几个数值,去代表某种状态
  • 特点
    • 可以给一组数值分别取上一个更好理解的名字
    • 一个枚举值,只会存在几个固定的值,不会出现超出范围的可能性
  • 很多传统的编程语言当中,枚举是一个很常见的数据结构,但在JS中并没有这种数据结构
  • 很多时候我们都需要使用一个对象模拟实现枚举
const PostStatus = {
    Draft: 0,
    Unpublished: 1,
    Published: 2
}

const post = {
    title : 'hello TS',
    content: 'TS is a typed superset of JavaScript',
    status: PostStatus.Draft // 2 // 1 // 0
}
  • 在TS中用一个专门的枚举类型我们可以使用enum这个关键词去声明一个枚举
enum PostStatus {
    Draft = 0,
    Unpublished = 1,
    Published = 2
}

const post = {
    title : 'hello TS',
    content: 'TS is a typed superset of JavaScript',
    status: PostStatus.Draft // 2 // 1 // 0
}
  • 注意点
    • 这里使用的是 赋值号 而不是 冒号
    • 枚举类型的只可以不用去使用赋值号指定,不指定的话会默认从0开始累加,
    • 如果给枚举当中第一个成员指定了具体的值,会变得值会在这个基础之上累加
    • 枚举的值可以是number之外,也可以是string,也就是字符串枚举
      • 由于字符串无法像数字一样自增,所以如果是字符串枚举的话,需要我们手动给每一个成员初始化一个明确的初始化的值
      • 字符串枚举并不常见
    // enum PostStatus {
    //     Draft,
    //     Unpublished,
    //     Published
    // }
    
    // enum PostStatus {
    //     Draft = 6,
    //     Unpublished,
    //     Published
    // }
    
    enum PostStatus {
        Draft = 'a',
        Unpublished = 'b',
        Published = 'c'
    }
    
    enum PostStatus {
        Draft,
        Unpublished,
        Published
    }
    
    const post = {
        title : 'hello TS',
        content: 'TS is a typed superset of JavaScript',
        status: PostStatus.Draft // 2 // 1 // 0
    }
    
  • 关于枚举我们还需要了解一点
    • 枚举类型会入侵到我们运行时的代码
    • 换句话说就是:他会影响我们编译后的结果
    • 我们在TS中使用的大多数类型他在经过编译转换过后最终都会被移除掉,但是枚举不会,最终会编译为双向的键值对对象
      • 双向键值对 --- 通过键可以获取值,也可以通过值再去获取键
    • 目的是
      • 可以动态的根据枚举值,去获取枚举的名称,也就是说在代码当中可以通过索引器的方式去获取对应的名称
    • 如果确定在代码中不会使用索引器的方式去访问枚举,那我们建议大家使用常量枚举
      • 常量枚举的用法就是在enum这个关键词前加上一个const

函数类型

  • 对函数的输入输出进行限制,也就是参数与返回值
  • 在JS中有两种函数定义的方式
    • 函数声明
    • 函数表达式
  • 这里需要了解两种方式下函数如何进行类型的约束
export {}

// function fnuc (a: number, b: number): string {
//     return 'func'
// }
// 参数限制为number,返回值限制为string;形参与实参必须完全一致,从类型到数量
// fnuc(1, 2)

// 可选参数
// function fnuc (a: number, b?: number): string {
//     return 'func'
// }
// fnuc(1)

// 或者使用参数默认值,也可以让其成为可选参数
// function fnuc (a: number, b: number = 100): string {
//     return 'func'
// }
// fnuc(1)

// 不管可选参数还是默认参数,都要出现在参数列表的最后
// 参数会按照位置进行传递,如果可选参数出在了必选参数的前边,那必选参数就没法拿到正确的参数

// 任意参数
function fnuc (a: number, b: number = 100, ...rest: number[]): string {
    return 'func'
}
fnuc(1)

// 函数表达式
const func1 = function (a: number, b: number): string {
    return 'func1'
}
/**
 * 这个函数表达式最终会放到一个变量当中的,接受这个函数的变量也应该是有类型的
 * 
 * 一般TS可以根据我们这个函数表达式推断出来我们变量的类型
 * 
 * 如果我们是把一个函数作为参数传递,也就是作为回调函数这种方式
 * 
 * 作为回调函数这种情况下,我们就必须要去约束这个回调函数这个参数的类型
 * 
 * 这种情况就可以使用类似箭头函数的方式去表示我们的参数可以接受什么样一个函数
 * 
 * 这种方式在定义接口时会经常用到
 * */ 

任意类型

  • 由于JS自身时弱类型的关系,很多内置的API本身就支持接收任意类型的参数
  • 而TS又是基于JS之上的,所以我们难免会在代码中需要用一个变量去接收任意类型的数据
  • 注意:
    • any类型仍然属于动态类型,特点和普通的JS变量是一样的,可以接受任意类型的数值,而且在运行过程当中还可以接受其它类型的值
  • 因为他有可能会存放任意类型的值,所以说TS他不会对any这种类型做类型检查,这也就意味着,我们仍然可以像在JS一样在他上调用任意的成员,语法上都不会报错
  • 但他也还是会存在类型安全的问题,所以我们轻易不要使用这种类型
function string (value: any) {
    return JSON.stringify(value)
}

string('string')
string(100)

隐式类型推断

  • 在TS中,如果我们没有通过明确的通过类型注解去标记一个类型,那TS会根据这个变量的使用情况去推断这个变量的类型 --- 隐式类型推断
export {}

let age = 18// TS会判断为number类型
// age = 'tri'  // 报错原因:age已经被判断为number

// 这种用法相当于给age添加了 number的类型注解

// 无法推断时会将它标记为any,如:
let foo

foo = 18;
foo = 'string'
foo = true
  • 虽然TS中支持隐式类型判断,而且这种隐式类型推断可以帮我们简化一部分代码,但是我们仍然建议大家尽可能给每一个变量添加明确的类型,这样便于我们后期更直观的理解我们的代码

类型断言

  • 在一些特殊形况下TS无法推断出变量的具体类型,而我们作为开发者我们根据代码的使用情况,我们是可以明确知道这个变量到底是什么类型的
// 假定来自一个明确的接口
const nums = [110, 120, 119, 112]

const res = nums.find(i => i > 0)
/**
 * 我们这个时候是明确知道这个返回值一定是一个number的
 * 
 * 但是TS推断为了number或者undefined
 * 
 * 那这个返回值就不能作为一个number来使用
 * 
 * 这时我们可以去断言这个res为一个number类型的
 * */ 
  • 断言的方式
    1. 使用as关键词
    2. 使用<>
    const num1 = res as number
    /**
    * 此时编译器能明确知道我们的num1是一个number
    * */ 
    
    const num2 = <number>res
    /**
    * 两者效果一致,但是这种尖括号的方式她有个小问题
    * 
    * 当我们在代码当中使用了JSX的时候
    * 
    * 这里的<number>会和JSX中的标签产生语法冲突
    * 
    * 推荐使用as的方式
    * */  
    
  • 注意:
    • 类型断言并不是类型转换,这里并不是把一个类型转换为了另外一个类型,因为类型转换是代码在运行时的一个概念,我们这个地方类型的断言只是在编译过程中的一个概念,当代码编译过后,断言也就不会存在了

接口

  • 可以理解为一种规范或者契约,是一种抽象的概念,可以约定一种对象的结构,我们去使用一个接口就必须要遵循这个接口的全部的约定
  • 在TS当中接口最直观的体现就是可以约定一个对象当中具体应该有哪些成员,这些成员的类型又是什么样的
export {}

interface Post {
    /**
     * 这里可以使用逗号分隔,但是标准是用分号,分号也可以省略
     * */ 
    title: string;
    content: string
}

function pointPost (post: Post) {
    console.log(post.title)
    console.log(post.content)
}
/**
 * 这个函数对对象有一些要求,必须要title属性和content属性
 * 
 * 只不过这种要求实际上是隐形的,没有明确的表达
 * 
 * 这种情况就可以使用接口表现出来约束
 * */ 

pointPost({
    'title': 'hi, TS',
    'content': 'hi'
})

  • 一句话总结:接口就是用来约束对象的结构,一个对象去实现一个接口,他就必须要拥有这个接口当中所有的成员

接口补充

  • 对于接口中约定的成员还有一些特殊的用法
    • 可选成员
      • 如果说我们在一个对象当中,我们某一个成员他是可有可无的,我们对于约束这个对象的接口,我们可以使用可选成员这样一个特性
      • 这种用法其实就相当于对其类型标记了一个指定类型或者是undefined
      interface Post {
          title: string;
          content: string;
          subtitle?: string
      }
      
      const hello:Post = {
          'title': 'hi, TS',
          'content': 'hi'
      }
      
    • 只读成员
      • 初始化过后就不可再次修改
      interface Post {
          title: string;
          content: string;
          subtitle?: string;
          readonly summary: string
      }
      
      const hello:Post = {
          'title': 'hi, TS',
          'content': 'hi',
          'summary':'初始化后不可修改'
      }
      // hello.summary = 'hhhhhhhh'  // 报错
      
    • 动态成员
      • 一般适用于一些具有动态成员的对象,例如程序中的缓存对象,他在运行当中就会出现一些动态的键值
      interface Cache {
          [key: string]: string
          // key是不固定的,string是key的类型注释,外边的string是设置给这个动态属性的值
      }
      
      const cache: Cache = {}
      
      cache.foo = 'value'
      cache.foo = '123'
      

类的基本使用

  • Classes
  • 描述一类具体事务的抽象特征
  • 例如手机就是一个类,特征就是可以接打电话,收发短信,在这个类型下,还会有一些细分的子类,这种子类她一定会满足父类的所有特征,然后多出一些额外的特征,例如智能手机,除了接打电话收发短信,还可以使用一些APP
  • 我们是不能直接使用这个类的,而是使用这个类的具体事务,比如你手上的智能手机
  • 类也是一样的,它可以用来描述一类具体对象的抽象成员,在ES6以前,JS都是通过 函数+原型 模拟实现的类,ES6之后JS就有了专门的class
  • 在TS中,除了可以使用所有ES标准当中所用类的功能,还添加了一些额外的功能与用法,例如对类成员有特殊的访问修饰符,还有一些抽象类的概念
class Person {
    /**
     * 在TS中,类的属性必须要有一个初始值可以在 = 后边赋值
     * 
     * 也可以在构造函数中初始化
     * 
     * 两者必须有其中一个
     * */ 
    name: string // = '初始值'
    age: number // = 123
    constructor (name: string, age: number) {
        /**
         * 我们在TS中我们需要明确在类型当中去声明他所拥有的一些属性
         * 
         * 而不是直接在构造函数当中动态通过this添加
         * */ 
        this.name = name
        this.age = age
    }
}
  • 类的属性在使用之前,必须要先在类型中去声明,目的其实就是为了给我们的属性去做一些类型的标注,除此之外,我们可以按照ES6当中的语法为这个类去声明一些方法

类的访问修饰符

  • private --- 标注为私有属性,只能在类的内部访问到
export {}

class Person {
    name: string
    private age: number // 标注为私有属性,只能在类的内部访问到

    constructor (name: string, age: number) {
        this.name = name
        this.age = age
    }
}

const tom = new Person('tom', 18)
console.log(tom.name)
// console.log(tom.age)    // 报错
  • public --- 标注为公有成员,TS中为默认!
export {}

class Person {
    ...
    private age: number // 标注为私有属性,只能在类的内部访问到
    ...
}

const tom = new Person('tom', 18)
// console.log(tom.age)    // 报错
  • protected --- 受保护的,外部访问不到
export {}

class Person {
    ...
    protected gender: boolean
    ...
}

const tom = new Person('tom', 18)
console.log(tom.gender) // 报错,不能在外部被访问到
  • private 与 protected 区别

    • protected只允许在此类当中去访问对应的成员
    export {}
    
    class Person {
        public name: string // 标注为公有成员,TS中为默认!
        private age: number // 标注为私有属性,只能在类的内部访问到
        protected gender: boolean
    
        constructor (name: string, age: number) {
            this.name = name
            this.age = age
            this.gender = true
        }
    }
    
    class Student extends Person {
        constructor(naem: string, age: number) {
            super(name, age)
            console.log(this.gender)    // 可以访问到
        }
    }
    
  • 作用:

    • 控制一些类当中成员的可访问级别
  • 注意点:

    • 对于构造函数的访问修饰符默认是public,如果设置为private,这个类型就不能在外部被实例化,也不能被继承,这种情况我们就只能在这个类的内部去添加一个静态方法,然后在静态方法中去创建这个类型的实例,因为private只允许在内部去访问
    class Student extends Person {
        private constructor(naem: string, age: number) {
            super(name, age)
            console.log(this.gender)    // 可以访问到
        }
    
        static create (name: string, age: number) {
            return new Student(name, age)
        }
    }
    // const jack = new Student()
    const jacked = Student.create('jack', 18)
    
    • 如果把构造函数当中的类型标记为protected,这样一个类型也是不能够在外部被实例化的,但是相比于private,他是允许被继承的

类的只读属性

export {}

class Person {
    public name: string 
    private age: number 
    protected readonly gender: boolean

    constructor (name: string, age: number) {
        this.name = name
        this.age = age
        this.gender = true
    }

    sayHi (msg: string): void {
        console.log(`我是${this.name},${msg}`)
    }
}


const tom = new Person('tom', 18)
console.log(tom.name)
tom.gender = false

类与接口

  • 相比于类,接口的概念要更抽象一点
  • 我们接着拿之前手机的例子来去作对比,那我们说手机是一个类型,这个类型的实例都是可以接打电话收发短信,因为手机这个类的特征就是这样,但是我们能够打电话的不仅只有手机,之前还有座机也可以打电话,但是座机并不属于手机这个类目,而是个单独的类目,因为他不能收发短信,在这种情况下,就会出现不同的类与类之间出现共同的特征,对于这些公共的特征,我们一般会使用接口去抽象,手机可以理解为,手机也能接打电话,因为它实现了这个协议,座机也可以打电话,因为他也实现了这个协议,这里说的协议,程序当中就叫做接口
export {}

interface Eat {
    eat (food: string): void
}

interface Run {
    run (distance: number): void
}

class Person implements Eat, Run {
    eat (food: string): void {
        console.log(`优雅的进餐:${food}`)
    }
    run (distance: number): void {
        console.log(`直立行走:${distance}`)
    }
}

class Animal implements Eat, Run {
    eat (food: string): void {
        console.log(`呼噜呼噜的吃:${food}`)
    }
    run (distance: number): void {
        console.log(`爬行:${distance}`)
    }
}

抽象类

  • 某种程度上和接口有点类似,它也可以用来去约束子类当中必须要有某一个成员
  • 抽象类它可以包含一些具体的实现,而接口只能是成员的一个抽象,不包含具体的实现
  • 一般比较大的类目比较建议使用抽象类,例如刚才的动物类,其实他就应该是抽象的,因为我们所说的动物只是一个泛指,并不够具体,在他的下边一定会有一些更细化的分类
abstract class Animal {
    eat (food: string): void {
        console.log(`呼噜呼噜的吃:${food}`)
    }

    abstract run (distance: number): void
}

// 被定义为抽象类过后,他就只能够被继承,不能够再使用new的方式去实例

class Dog extends Animal {
    run (distance: number): void {
        console.log('四角爬行',distance)
    }
}

const d = new Dog()
d.eat('狗')
d.run(100)

泛型

  • 定义函数,接口,或者类的时候,我们没有去指定具体的类型,等到我们使用的时候,再去指定具体的类型的一个特征
  • 以函数为例,泛型就是我们在声明这个函数时,不去指定具体的类型,等到调用时传递一个具体的类型,目的是为了最大程度的复用我们的代码
export {}

// 创建一个指定长度的数组
// function createNumberArray (length: number, value: number): number[] {
//     const arr = Array<number>(length).fill(value)
//     return arr   
// }

// const res = createNumberArray(3, 100)

function createArray<T> (length: number, value: T): T[] {
    const arr = Array<T>(length).fill(value)
    return arr
}

const res = createArray<number>(3, 100)
const res1 = createArray<string>(3, '100')
  • 总结:
    • 泛型其实就是把我们定义时不能够明确的类型,改为一个参数,让我们在使用的时候再去传递

类型声明

  • 实际的项目开发中,我们肯定会用到一些第三方的npm模块,而这些模块并不是一定通过TS编写的,所以它所提供的成员就不会有强类型的体验
  • 说白了就是一个成员在定义的时候因为某种原因导致没有明确的类型,然后我们使用时再为他单独做出一个明确的声明,这种用法存在的原因就是为了考虑兼容一些普通的JS模块,由于TS的社区非常的强大,目前绝大多数常用的npm模块都已经提供了对应的声明,我们只需要安装一下它所对应的类型声明模块即可
  • 如果没有对应的声明,我们就只能通过declare语句去声明所对应的模块类型
import {camelCase} from 'lodash' 

declare function camelCase (input: string): string

const res = camelCase('hello typed')