前端类型系统

485 阅读11分钟

JavaScript是一门弱类型且动态类型的语言,这在小项目的开发中尤为便捷,但在大型项目中没有类型的约束维护起来会让人十分头疼,本篇文章会详细描述强弱类型系统的区别,以及如何为前端增加类型系统

类型系统

从类型安全角度出发,类型系统分为强类型弱类型,从类型检查维度出发,类型系统分为静态类型动态类型

强类型和弱类型

从语言层面讲,强类型限制函数的实参类型必须与形参类型相同,而弱类型不会限制,即便函数需要的是整型,而我们传入一个字符串,在语法上也不会报错,但运行时可能会出现错误。

强类型语言中不允许任意的隐式数据转换,而弱类型中允许有任意的隐式数据转换

变量类型允许随时改变的特点不是强弱类型的差异,python就是一个例子。

静态类型和动态类型

静态类型指变量在声明是它的类型就已经确定,而且在声明过后,他的类型就不允许再修改。

动态类型语言的特点在运行阶段才明确一个变量的类型,而且变量的类型随时可以改变。

Javascript的类型系统

JavaScript是一种弱类型同时是动态类型的语言,它非常的灵活多变,但灵活多变也会让他变得‘不靠谱’,因为在设计之初JavaScrpit只是为了解决一些简单的问题,几十行几百行代码就能搞定,这个时候如果设计成强类型就会让javaScript显得很臃肿。同时javascript没有编译环节,如果把它设计成静态类型语言也没有意义,因为静态类型语言需要在编译阶段做类型检查,而javascript没有这样一个环节。所以在当时,javascript是弱类型静态类型语言并没有什么问题,甚至可以说灵活多变是他的一个优势。但现在的前端项目规模越来越大,js代码越来越复杂,开发周期越来越长,js的缺点开始显露出来。就像js是一把水果刀,用来它来削水果十分顺手,而现在要用它来宰牛了,就显得力不从心了。

JavaScript弱类型带来的问题

下面列举一些javaScript弱类型带来的一些隐患

异常在运行时才会暴露出来

js的代码可能会调用一个对象中未定义的函数,如果这个代码在异步的回调中一直未被测试到,那么这个隐患只会在运行到这行代码才暴露出来。

// JavaScript 弱类型产生的问题
// 1. 异常需要等到运行时才能发现
const obj = {}
// obj.foo()
setTimeout(() => {
  obj.foo()
}, 1000000)

如果是强类型的语言,在编译过程中就会报错,不用等到运行到这行代码。

函数功能可能发生改变

如果调用的时候传入的是数字,那么返回的结果就是数字的和,而如果传入的是字符串,那么返回的结果就是字符串相加的结果。

// 2. 函数功能可能发生改变

function sum (a, b) {
  return a + b
}
console.log(sum(100, 100))
console.log(sum(100, '100'))  

在多人协同开发的项目中,单纯的口头约定很难做到传入的参数类型一致

对象索引器的错误用法

对象属性名会隐式的转换成string,导致调用的时候传字符串也能拿到相应的值

const obj = {}

obj[true] = 100 // 属性名会自动转换为字符串

console.log(obj['true'])

当然,这些问题只是javaScript弱类型问题的冰山一角,当代码量大的时候弱类型的缺陷会更加明显,强类型的约束可以提前消灭大部分异常,而不必等到运行时再debug。

强类型的优势

  1. 错误可以更早暴露。
  2. 代码提示更智能,编码更准确。编辑器会知道每个对象中的属性,代码更有效率。
  3. 重构更可靠。当修改成员变量名称的时候,如果有忘记修改的地方编辑器会报错,甚至会自动修改。
  4. 减少不必要的类型判断,比如在函数参数中提前指明接受参数的类型,则不需要在函数内部去写类型判断了。

Flow

flow是一个JavaScript的类型检查器,在react、vue中被广泛使用。他的工作原理就是在代码中标记变量或参数的类型,它的原理就是在代码中判断传入的参数是否和规定的参数一致从而实现在开发阶段进行类型约束。 比如规定在sum函数的参数中传入number值就可以使用变量后面加: number的形式去约束变量类型。

// @flow

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

sum(100, 100)

// sum('100', '100')

// sum('100', 100)

如果传入的不是数字,编辑器会报错。

编译过后可以通过babel再转回JavaScript代码,从而不影响程序运行。

起始

flow是以npm模块使用的,首先可以使用yarn add flow-bin --devnpm install flow-bin --dev将flow安装在开发依赖中。

然后通过yarn flow init去初始化flow配置文件,生成.flowconfig文件。

第一次启用需要使用yarn flow启动flow后台服务,停止flow命令使用yarn flow stop

要启用flow需要在文件开头加上

// @flow

这个时候编辑器还是会报错,需要关闭vs code的JavaScript代码检查。

移除代码注解

类型注解并不是js的语法,所以在添加注解后运行代码会报错,需要在运行时自动移除类型注解。

flow-remove-types工具

使用 npm包flow-remove-types工具可以自动移除类型注解。

安装包过后运行yarn flow-remove-types <代码目录> -d <输出目录>执行过后就会在输出目录生成移除注解的js文件。一般开发过程中,我们将源代码放在src目录,移除注解的目录放在dist目录。避免我们在转换的过程中删除第三方模块的注解。

babel

使用babel转换代码需要@babel/core @babel/cli @babel/preset-flow工具,安装之后就可以使用babel转换代码。

首先要在根目录添加babel配置文件.babellrc,在配置文件中添加presets的配置,添加上刚刚flow的preset。

{
  "presets": ["@babel/preset-flow"]
}

然后使用yarn babel src -d dist就可以转换代码了。

如果已经使用babel,那么使用babel去移除注解是更好的方式。

Flow开发工具插件

使用vs code的flow language support在vscode中可以直接显示代码类型的异常。

类型推断

flow可以对函数中进行的操作去隐式的推断类型,比如

function square (n) {
  return n * n
}

square('100')// 报错

因为字符串不能进行乘法运算,flow会将参数隐式推断为number类型,当传入字符串时就会报错。

在开发过程中最好添加类型注解,提升代码可读性。

类型注解

类型注解可以用在变量,函数参数,返回值中。

function square (n: number) {
  return n * n
}

let num: number = 100

// num = 'string' // error

function foo (): number {
  return 100 // ok
  // return 'string' // error
}

function bar (): void {
  // return undefined
}
  1. 原始数据类型 string类型

    const a: string = 'foobar'
    

    number类型 可以存放NaN ,Infinity无穷大

    const b: number = Infinity // NaN // 100
    

    bool类型

    const c: boolean = false // true
    

    null

    const d: null = null
    

    undefined

    const e: void = undefined
    

    Symbol类型

    const f: symbol = Symbol()
    
  2. 数组类型

    数组类型有两种注解方式

    const arr1: Array<number> = [1, 2, 3] //使用泛型
    const arr2: number[] = [1, 2, 3]
    

    元组类型

    // 元组
    const foo: [string, number] = ['foo', 100]
    
  3. 对象类型 可以指定对象的属性,和属性的类型,可选参数后面加?,不指定属性名则使用中括号string。

    const obj1: { foo: string, bar: number } = { foo: 'string', bar: 100 }
    
    const obj2: { foo?: string, bar: number } = { bar: 100 }
    
    const obj3: { [string]: string } = {}
    
    obj3.key1 = 'value1'
    obj3.key2 = 'value2'
    
  4. 函数类型 函数也可以指定类型,使用箭头方式括号内是参数的类型,箭头指向返回值的类型。

function foo (callback: (string, number) => void) {
  callback('string', 100)
}

foo(function (str, n) {
  // str => string
  // n => number
})
  1. 特殊类型
    • 字面量类型

      例如只能存放固定字符串,一般会联合使用

      const a: 'foo' = 'foo'
      const type: 'success' | 'warning' | 'danger' = 'success'
      

      这种|或标识符不仅仅可以用在字面量类型还可以用在其他类型上,比如规定只能为数字或number

    • 声明类型 用type声明一个类型

      type StringOrNumber = string | number
      const b: StringOrNumber = 'string' // 100
      
    • Mybee类型 当我们遇到数字类型同时可能为空的时候可以在声明前加一个问号,表示可以为null或undefined

      // Maybe 类型
      const gender: ?number = undefined
      // 相当于
      const gender: number | null | void = undefined
      

6.mixed类型和any类型 Mixed类型是所有类型的联合类型,可以传入任意参数,同样any也可以传入任意类型

// string | number | boolean | ....
function passMixed (value: mixed) {
 if (typeof value === 'string') {
   value.substr(1)
 }

 if (typeof value === 'number') {
   value * value
 }
}
function passAny (value: any) {
 value.substr(1)

 value * value
}

passAny('string')

passAny(100)

要注意的是此处的mixed是强类型,而any是弱类型, mixed类型需要做类型判断,any的存在是为了兼容一些老的代码。

Flow 运行环境API

浏览器中的DOM、BOM,node中的模块这些API,flow也能对类型进行限制,比如document.getElementById接受的是字符串

const element: HTMLElement | null = document.getElementById('app')

TypeScript

TypeScript是JavaScript的一个超集,多出来的就是类型系统的部分,还有对ECMAScript新特性的支持,TypeScript经过编译还是会生成一个JavaScript。 image.png TypeScript的类型系统和Flow是很类似的。

TypeScript支持对ECMAScript新特性的转换,可以把ES新特性代码最低转换成ES3的代码去运行,即便我们不需要类型系统,通过TypeScript去使用ECMAScript的新特性也是很好的选择,在这方面TypeScript实现的功能和Babel是类似的。

因为TypeScript最终会转换成JavaScript,所以任何一种Javascript的运行环境都可以使用TypeScript去开发。

TypeScript的生态比Flow功能更强大,生态也更健全,更完善,特别是VS Code对TypeScript的支持特别友好,Angular Vue3.0都已经转向使用TypeScript开发。

任何一门语言都有缺点,TypeScript相比JavaScript增加了很多学习成本,更为复杂,但TypeScript是一门渐进式语言,即使完全不了解TypeScript,也可以使用纯JavaScript语言去开发,在一些特别小的项目的项目初期,TypeScript相比Javascript会增加一些成本,但大型项目上,TypeScript会比JavaScript更可靠,同时比JavaScript开发效率更高。

上手

在项目根目录通过yarn add typescript --dev去安装,安装过后,在node_modules的.bin文件夹下会出现一个tsc(TypeScriptCompiler)的命令,这个命令会帮助我们去编译TypeScript代码。 使用es6的箭头函数编写一个typeScript文件,然后尝试编译一下。

// ts文件
// 可以完全按照 JavaScript 标准语法编写代码

const hello = (name: any) =>  {
  console.log(`Hello, ${name}`)
}

hello('TypeScript')
// js文件
"use strict";
// 可以完全按照 JavaScript 标准语法编写代码
var hello = function (name) {
    console.log("Hello, " + name);
};
hello('TypeScript');

这个时候会发现编译生成的ts文件的es6部分都被自动转换成了es3的语法,同时类型声明都被隐藏了。

如果我们传入了和声明类型不一致的参数,vs code会自动报错,同时编译也会不通过。

配置文件

通过yarn tsc --init命令去生成一个TypeScript配置文件.tsconfig.json


{
  "compilerOptions": {
      "incremental": true,                // 增量编译
      "tsBuildInfoFile": "./buildFile",   // 增量编译文件的存储位置
      "diagnostics": true,                // 打印诊断信息
 
      "target": "es5",           // 目标语言的版本
      "module": "commonjs",      // 生成代码的模块标准
      "outFile": "./app.js",     // 将多个相互依赖的文件生成一个文件,可以用在 AMD 模块中
 
      "lib": [],                 // TS 需要引用的库,即声明文件,es5 默认 "dom", "es5", "scripthost"
 
      "allowJs": true,           // 允许编译 JS 文件(js、jsx)
      "checkJs": true,           // 允许在 JS 文件中报错,通常与 allowJS 一起使用
      "outDir": "dist",         // 指定输出目录
      "rootDir": "src",           // 指定输入文件目录(用于输出)
 
      "declaration": true,         // 生成声明文件
      "declarationDir": "./d",     // 声明文件的路径
      "emitDeclarationOnly": true, // 只生成声明文件
      "sourceMap": true,           // 生成目标文件的 sourceMap
      "inlineSourceMap": true,     // 生成目标文件的 inline sourceMap
      "declarationMap": true,      // 生成声明文件的 sourceMap
      "typeRoots": [],             // 声明文件目录,默认 node_modules/@types
      "types": [],                 // 声明文件包
 
      "removeComments": true,    // 删除注释
 
      "noEmit": true,            // 不输出文件
      "noEmitOnError": true,     // 发生错误时不输出文件
 
      "noEmitHelpers": true,     // 不生成 helper 函数,需额外安装 ts-helpers
      "importHelpers": true,     // 通过 tslib 引入 helper 函数,文件必须是模块
 
      "downlevelIteration": true,    // 降级遍历器的实现(es3/5)
 
      "strict": true,                        // 开启所有严格的类型检查
      "alwaysStrict": false,                 // 在代码中注入 "use strict";
      "noImplicitAny": false,                // 不允许隐式的 any 类型
      "strictNullChecks": false,             // 不允许把 null、undefined 赋值给其他类型变量
      "strictFunctionTypes": false           // 不允许函数参数双向协变
      "strictPropertyInitialization": false, // 类的实例属性必须初始化
      "strictBindCallApply": false,          // 严格的 bind/call/apply 检查
      "noImplicitThis": false,               // 不允许 this 有隐式的 any 类型
 
      "noUnusedLocals": true,                // 检查只声明,未使用的局部变量
      "noUnusedParameters": true,            // 检查未使用的函数参数
      "noFallthroughCasesInSwitch": true,    // 防止 switch 语句贯穿
      "noImplicitReturns": true,             // 每个分支都要有返回值
 
      "esModuleInterop": true,               // 允许 export = 导出,由import from 导入
      "allowUmdGlobalAccess": true,          // 允许在模块中访问 UMD 全局变量
      "moduleResolution": "node",            // 模块解析策略
      "baseUrl": "./",                       // 解析非相对模块的基地址
      "paths": {                             // 路径映射,相对于 baseUrl
        "jquery": ["node_modules/jquery/dist/jquery.slim.min.js"]
      },
      "rootDirs": ["src", "out"],            // 将多个目录放在一个虚拟目录下,用于运行时
 
      "listEmittedFiles": true,        // 打印输出的文件
      "listFiles": true,               // 打印编译的文件(包括引用的声明文件)
  }
}

TS原始类型

在非严格模式下 String number boolean类型可以为空

// 原始数据类型

const a: string = 'foobar'

const b: number = 100 // NaN Infinity

const c: boolean = true // false

// 在非严格模式(strictNullChecks)下,
// string, number, boolean 都可以为空
// const d: string = null
// const d: number = null
// const d: boolean = null

const e: void = undefined

const f: null = null

const g: undefined = undefined
// Symbol 是 ES2015 标准中定义的成员,
// 使用它的前提是必须确保有对应的 ES2015 标准库引用
// 也就是 tsconfig.json 中的 lib 选项必须包含 ES2015
const h: symbol = Symbol()

Symbol是ES6新增的内置标准对象,如果在tsconfig.json中设置target为es5以下,那么typescript引用的就是es5以下的标准库,es5的标准库中是没有Symbol的,同样Promise这些ES6的新特性也是没有的。这里我们有两种方法解决,可以将target改为es2015,也可以使用配置文件中的lib,加入es2015的lib库就可以。假如我们要使用console对象,需要在lib中加入DOM。BOM和DOM对象都被归在了DOM中。

"lib": ["ES2015", "DOM"],  

中文报错消息

我们可以通过yarn tsc --locale zh-CN命令,把命令行中的编译报错修改为中文。

vs code中的报错消息可以通过vs code的typescript设置修改为中文

image.png 虽然可以修改报错消息,但是不推荐这么做,因为很多错误在Google上的解决方案都是英文的,使用英文更易查找解决方案。

作用域问题

在多文件开发时,不同文件中定义名字相同的变量会报错。

可以通过自执行函数来生成一个函数作用域

// 解决办法1: IIFE 提供独立作用域
(function () {
  const a = 123
})()

也可以在文件的最后台添加一句export {},意思是这是一个模块而不是导出了一个空对象,这样文件中的所有成员都变成了模块作用域中的局部成员了。

// 解决办法2: 在当前文件使用 export,也就是把当前文件变成一个模块
// 模块有单独的作用域
const a = 123

export {}

Object类型

Object类型指除了原始类型的其他类型,比如对象,数组

// object 类型是指除了原始类型以外的其它类型
const foo: object = function () {} // [] // {}

// 如果需要明确限制对象类型,则应该使用这种类型对象字面量的语法,或者是「接口」
const obj: { foo: number, bar: string } = { foo: 123, bar: 'string' }

如果要明确是一个对象,以及对象中的成员和成员的类型,需要使用{}的形式,但一般在开发中,不使用这种方式去声明对象的类型,而是使用接口。

数组类型

数组有两种表示方式

// 数组类型的两种表示方式

const arr1: Array<number> = [1, 2, 3]

const arr2: number[] = [1, 2, 3]

使用ts可以对数组类型的参数进行限制,省去了类型判断

// 如果是 JS,需要判断是不是每个成员都是数字
// 使用 TS,类型有保障,不用添加类型判断
function sum (...args: number[]) {
  return args.reduce((prev, current) => prev + current, 0)
}

sum(1, 2, 3) // => 6

元组类型

元组就是明确元素数量和元素类型的数组

const tuple: [number, string] = [18, 'zce']

可以使用数组下表去访问,也可以解构元组

const age = tuple[0]
const name = tuple[1]

const [age, name] = tuple

Object.entries方法返回的键值数组就是元组

const entries: [string, number][] = Object.entries({
  foo: 123,
  bar: 456
})

枚举类型

在js中可以使用对象去模拟数组

// 用对象模拟枚举
const PostStatus = {
  Draft: 0,
  Unpublished: 1,
  Published: 2
}

在ts中可以使用enum关键字定义一个枚举,使用枚举和对象一样。

// 标准的数字枚举
enum PostStatus {
  Draft = 0,
  Unpublished = 1,
  Published = 2
}

枚举可以不指定等号的值,可以根据指定的值往后累加。

// 数字枚举,枚举值自动基于前一个值自增
enum PostStatus {
  Draft = 6,
  Unpublished, // => 7
  Published // => 8
}

可以定义字符串枚举,字符串枚举无法自增,所以必须指定每一个字符串的值。

// 字符串枚举
enum PostStatus {
  Draft = 'aaa',
  Unpublished = 'bbb',
  Published = 'ccc'
}

TypeScript将枚举编译成es语法就是先给枚举的的键赋值,再把值赋值给键,就可以动态的根据枚举值或者键去获取

var PostStatus;
(function (PostStatus) {
    PostStatus[PostStatus["Draft"] = 6] = "Draft";
    PostStatus[PostStatus["Unpublished"] = 7] = "Unpublished";
    PostStatus[PostStatus["Published"] = 8] = "Published"; // => 8
})(PostStatus || (PostStatus = {}));

如果代码中不会使用索引器的方式访问枚举,那么可以定义常量枚举,就是在枚举面前添加const

// 常量枚举,不会侵入编译结果
const enum PostStatus {
  Draft,
  Unpublished,
  Published
}

const post = {
  title: 'Hello TypeScript',
  content: 'TypeScript is a typed superset of JavaScript.',
  status: PostStatus.Draft // 3 // 1 // 0
}

这个时候编译成的js中,枚举就会被替换成当前枚举的数值,并且后面跟上它的注释

var post = {
    title: 'Hello TypeScript',
    content: 'TypeScript is a typed superset of JavaScript.',
    status: 0 /* Draft */ // 3 // 1 // 0
};

函数类型

在ts中对函数的约束就是对函数的参数和返回值进行限制,在js中有两种函数的表示方式,分别是函数的声明和函数表达式。

函数声明
可以在参数后面加问号去表示参数为可选参数,可选参数必须在最后
可以使用es6的rest操作符来传入任意个数的参数
function func1 (a: number, b?: number = 10, ...rest: number[]): string {
  return 'func1'
}

func1(100, 200)

func1(100, 200, 300)
函数表达式

可以使用箭头函数的方式去指定函数传入的参数和返回的值

const func2: (a: number, b: number) => string = function (a: number, b: number): string {
  return 'func2'
}

TypeScript任意类型

any是动态类型,可以接受任何类型,ts不会对any进行类型检查

function stringify (value: any) {
  return JSON.stringify(value)
}

因为any类型不会做类型检查所以轻易的不要使用any类型。

隐式类型推断

如果没有标注类型,typescript会隐式的推断类型

比如赋值给age一个number,再给他赋值string就会报错

let age = 18 // number

age = 'string'

如果typescript不知道变量的类型,那么他会将类型自动推断为any。

虽然typescript支持隐式类型推断,但还是建议表明类型,这样才能让代码更直观。

类型断言

typescript无法直接的推断变量的具体类型,例如当变量来自一个明确类型的接口时,ts并不知道它返回的值一定是存在的

// 假定这个 nums 来自一个明确的接口
const nums = [110, 120, 119, 112]

const res = nums.find(i => i > 0)

const square = res * res

ts会认为res的值可能为undefined,断言的意思就是告诉ts res的类型是确定的。 此时我们可以用as关键词或者尖括号去断言类型

const num1 = res as number

const num2 = <number>res // JSX 下不能使用

尖括号在tsx、jsx语法中无法使用。

类型断言并不是类型转换,只是在编译过程中的类型声明,编译过后断言不存在

接口

接口是一种规范,可以去约定对象的结构,比如对象的成员和类型。

interface Post {
  title: string;
  content: string
}

function printPost (post: Post) {
  console.log(post.title)
  console.log(post.content)
}

printPost({
  title: 'Hello TypeScript',
  content: 'A javascript superset'
})

定义接口的成员可以使用分号去区分成员。

接口的成员

可选成员

可以使用问号去标记一个成员是可选的

interface Post {
  title: string
  content: string
  subtitle?: string
}
只读成员

使用readonly关键词去标记一个成员是只读成员,成员在初始化后就不可修改

interface Post {
  title: string
  content: string
  readonly summary: string
}
动态成员

例如缓存成员的键是不确定的,可以使用中括号去约束键名

interface Cache {
  [prop: string]: string
}

const cache: Cache = {}

cache.foo = 'value1'
cache.bar = 'value2'

类是描述一类具体事物的抽象特征

es6以前使用function+原型去模拟实现的类的特征,es6以后js中有了专门的class,ts中增强了class相关的语法

可以给类的成员赋初始值,更多的是给类

class Person {
  name: string // = 'init name'
  age: number
  
  constructor (name: string, age: number) {
    this.name = name
    this.age = age
  }

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

ts中需要提前声明类的属性,而不能直接在构造函数中通过this去添加。

ts中必须为类的属性提前声明

类的访问修饰符

TS中有public、protected、private访问修饰符,可以去控制类当中属性的访问修饰级别

  • 成员默认是public类型。
  • 私有属性只有在类的内部去访问。
  • protected类型只有在类和子类的内部去访问。
class Person {
  public name: string // = 'init name'
  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(`I am ${this.name}, ${msg}`)
    console.log(this.age)
  }
}

class Student extends Person {
  private constructor (name: string, age: number) {
    super(name, age)
    console.log(this.gender)
  }

  static create (name: string, age: number) {
    return new Student(name, age)
  }
}

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

const jack = Student.create('jack', 18)

如果构造函数被标记为private,那么只有在类提供的静态方法中去创建对象。

类的只读属性

可以使用readonly属性去标记类的属性是只读的,readonly属性要放在访问修饰符的后面。

class Person {
  public name: string // = 'init name'
  private age: number
  // 只读成员
  protected readonly gender: boolean
}

readonly的属性只能在声明的时候初始化,或者声明的时候未初始化在构造函数中初始化,声明过后不能修改。

类与接口

类与类之间的共同特征可以通过接口去抽象出来,一个类可以实现多个接口。

比如人和动物都会吃,都会跑,但人和动物不是一类,人和动物吃和跑的方式不一样。

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) {
    console.log(`直立行走: ${distance}`)
  }
}

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

  run (distance: number) {
    console.log(`爬行: ${distance}`)
  }
}

接口的抽象要尽量细化。

抽象类

抽象类和接口类似,也是约束成员,区别是抽象类可以有具体的实现。

抽象类可以定义抽象方法,继承自抽象类的子类必须实现抽象类的方法。

abstract class Animal {
  eat (food: string): void {
    console.log(`呼噜呼噜的吃: ${food}`)
  }

  abstract run (distance: number): void
}

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

}

const d = new Dog()
d.eat('嗯西马')
d.run(100)

泛型

在定义函数、接口或类的时候没有指定具体的类型,等到使用的时候再去指定类型,目的就是为了极大程度的复用代码。

在函数名中使用尖括号,传入泛型参数,一般泛型参数用T去命名,函数中不确定的类型都可以使用T去代表。

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

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

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

const res = createArray<string>(3, 'foo')

Array就是一个泛型类,需要用尖括号去指明array存放数据的类型。

TypeScript类型声明

在使用typescript中难免使用到npm包,常用的npm包都有专门的类型声明模块,有些npm包则没有类型声明,这个时候就要使用declare语法去声明类型。

import { camelCase } from 'lodash'

declare function camelCase (input: string): string

const res = camelCase('hello typed')

如果有类型声明模块,则只需要安装一下类型声明模块就有类型声明了。