强类型:语言层面限制函数的实参类型必须与形参类型相同。 弱类型:语言层面不会限制实参的类型。
这种强弱类型之分根本不是某一个权威机构的定义,但是公认的理解为:强类型有更强的类型约束,而弱类型中几乎没有什么约束。
强类型语言中不允许任意的隐式类型转换,而弱类型语言则允许任意的数据隐式类型转换。我们这里所说的强类型是从语言的语法层面就限制了不允许传入不同类型的值。如果传入的是不同类型的值,在编译阶段就会报出错误,而不是等到运行阶段再通过逻辑判断去限制。在JavaScript当中所有报出的类型错误都是在代码层面运行时通过逻辑判断手动抛出的。 而在JS代码当中,变量类型允许随意改变的特点,不是强弱类型的差异。就拿Python来说,它是一门强类型语言,但它的变量仍然是可以随时改变类型的。
静态类型:一个变量声明时它的类型就是明确的,声明过后,它的类型就不允许再修改。 动态类型:运行阶段才能够明确变量类型,而且变量的类型随时可以改变。
动态类型语言中的变量没有类型,而变量中存放的值是有类型的。大规模应用下,JavaScript早期的优势就变成了短板
综上,弱类型语言的弊端是十分明显的,只是在代码量小的情况下这些问题都可以通过约定的方式去规避,而对于一些开发周期特别长的大规模项目,这种‘君子约定’的方式仍然存在一定隐患,只有在语法层面的强制要求下才能提供更可靠的保证。
让我们在代码中通过添加类型注解的方式去标记代码中的变量以及参数的类型,根据这些类型注解检查代码中是否存在类型使用异常,从而实现开发阶段对类型异常的检查,避免了直到运行阶段才发现类型使用的错误。
function sum (a: number, b: number) { // 类型注解 return a + b }对于代码中额外的类型注解,可以在运行之前通过Babel或者Flow官方提供的模块自动去除。
类型注解并不是JavaScript的标准语法,当我们添加类型注解过后,代码是无法正常运行的。我们可以使用工具在完成编码过后自动移除掉我们所添加的类型注解。
使用官方提供的flow-remove-types移除 1)yarn add flow-remove-types --dev 2) yarn flow-remove-types [] -d [] //第一个参数为源代码所在目录 第二个参数为输出目录 使用Babel配合插件移除 1) yarn add @babel/core @babel/cli @babel/preset-flow --dev 2) 手动在项目中添加babel配置文件 .babelrc 并在其中输入 { "presets": ["@babel/preset-flow"] } 3) yarn babel [] -d [] //第一个参数为源代码所在目录 第二个参数为输出目录目前这种方式下,Flow检测到的代码中的问题都是输出到控制台当中的,这种体验并不直观,更好的方式是在开发工具中直接显示出类型问题。 打开VSCode的插件面板搜索flow,安装一个叫做Flow Language Support 的插件,这是Flow官方所提供的。 安装过后,VSCode的状态栏就会显示Flow的工作状态,而且代码中的异常也可以直接标记为红色波浪线,默认情况下只有在修改完代码过后保存才会生效,对于其他编辑器:Flow官网给出的对所有编辑器插件的支持
根据代码使用情况,Flow可自动推断出变量的类型。
/** * 类型推断 * * @flow */ function square (n) { return n * n } // square('100') square(100)在非严格模式下,string, number, boolean 都可以为空。可以在配置文件修改strictNullChecks去仅对null进行限制。
const d: string = null const d: number = null const d: boolean = null const e: void = undefined const f: null = null const g: undefined = undefinedSymbol 是 ES2015 标准中定义的成员,使用它的前提是必须确保有对应的 ES2015 标准库引用。也可以改变配置文件的target为es2015,但如果要求编译后为es5以下,则可以通过修改tsconfig.json中的 lib 选项,使其包含 ES2015标准库文件。但与此同时要加上dom标准库(dom和bom合并为dom标准库)以防止其他报错。
const h: symbol = Symbol()解决办法1: IIFE 提供独立作用域
(function () { const a = 123 })()解决办法2: 在当前文件使用 export,也就是把当前文件变成一个模块,模块有单独的作用域
const a = 123 export {}如果我们需要普通的对象类型,我们需要去使用类似字面量的语法,但是更专业的方式是使用接口:
// 如果需要明确限制对象类型,则应该使用这种类型对象字面量的语法,或者是「接口」 const obj: { foo: number, bar: string } = { foo: 123, bar: 'string' } // 接口的概念后续介绍数组类型的两种表示方式
const arr1: Array<number> = [1, 2, 3] // 纯数字组成的数组 const arr2: number[] = [1, 2, 3] // 如果是 JS,需要判断是不是每个成员都是数字 ->typeof // 使用 TS,类型有保障,不用添加类型判断 ->类型注解 function sum (...args: number[]) { // reduce计算所有成员的总和 参数1:上次计算的结果 参数2:本次循环的当前值 return args.reduce((prev, current) => prev + current, 0) }JavaScript中并没有枚举这种数据结构,大部分场景我们可以使用对象去模拟
// 用对象模拟枚举 const PostStatus = { Draft: 0, Unpublished: 1, Published: 2 } const post = { title: 'Hello TypeScript', content: 'TypeScript is a typed superset of JavaScript.', status: PostStatus.Draft // 3 // 1 // 0 }在TypeScript有专门的enum枚举类型
// 标准的数字枚举 enum PostStatus { Draft = 0, // 使用等号 Unpublished = 1, Published = 2 }使用方式与对象相同PostStatus.Draft。如果不指定等号后面的值,默认枚举中的值从0累加。
// 字符串枚举 enum PostStatus { // 无法自增长 必须给定值 Draft = 'aaa', Unpublished = 'bbb', Published = 'ccc' } 枚举类型会入侵到运行时的代码(会影响编译后的结果)枚举类型最终会编译为一个双向的键值对对象(可以通过键获取值,并通过值获取键)。好处是可以动态的根据枚举值获取枚举名称。
var PostStatus; (function (PostStatus) { PostStatus[PostStatus["Draft"] = 0] = "Draft"; PostStatus[PostStatus["Unpublished"] = 1] = "Unpublished"; PostStatus[PostStatus["Published"] = 2] = "Published"; })(PostStatus || (PostStatus = {})); PostStatus[0] // => Draft如果代码中不会使用索引器的方式去访问枚举,则建议使用常量枚举
// 常量枚举,不会侵入编译结果 const enum PostStatus { Draft, Unpublished, Published } Object.defineProperty(exports, "__esModule", { value: true }); var post = { title: 'Hello TypeScript', content: 'TypeScript is a typed superset of JavaScript.', status: 0 /* Draft */ // 3 // 1 // 0 };在TypeScript当中,如果没有通过类型注解去标记一个变量的类型,TypeScript会根据变量的使用情况去推断这个变量的类型,这种特性叫做隐式类型推断。
let age = 18 // age 被推断为 number age = 'string' // age类型错误如果TypeScript无法推断一个变量具体的类型,这个时候会将其类型标记为any
let foo // any类型 foo = 100 foo = 'string'虽然TypeScript支持隐式类型推断,而且这种隐式类型推断可以简化代码,但是仍然建议为每个变量添加明确的类型标注,便于后期理解代码。
在一些特殊情况下,TypeScript无法推断一个变量的具体类型。而作为为开发者,我们根据代码的使用情况是可以明确知道代码的类型的
// 假定这个 nums 来自一个明确的接口 const nums = [110, 120, 119, 112] const res = nums.find(i => i > 0) // 返回值一定是一个数字 // 但是对于TypeScipt推断出来的类型为number|undefined // 此时就无法将返回值直接当作数字使用 // const square = res * res // 此时我们可以断言res为number类型 // 1.使用as关键词断言 const num1 = res as number // 推荐 // 2.在变量前面使用尖括号断言 const num2 = <number>res // 问题:JSX 下不能使用 辅助TypeScript更加明确代码当中每一个成员的类型类型断言并不是类型转换(类型转换是代码运行时的概念,类型断言只是编译过程的概念)TypeScript 接口 最直观的体现:约定对象当中应该有哪些成员,成员的类型是什么样的
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' })TypeScript中的接口只是为有结构的数据做类型约束的,实际运行阶段并没有意义
可选成员、只读成员、动态成员
interface Post { title: string content: string subtitle?: string // 可选成员 '?' readonly summary: string // 只读成员 'readonly' } interface Cache { [prop: string]: string // 动态成员 'prop非固定' 第一个string键的类型 } const cache: Cache = {} cache.foo = 'value1' // string类型键值 cache.bar = 'value2'ES6以前,JavaScript通过函数配合原型的模式模拟实现类,ES6开始,JavaScript中有了专门的class。在TypeScript中,除了可以使用所有ECMAScript中所有类的功能还添加了一些额外的功能和用法。
TypeScript增强了class的相关语法 类的属性在使用之前必须在类型当中声明(为了给属性做类型标注) class Person { // 在TypeScript中需要明确在类型中声明所拥有的一些属性 name: string // = 'init name' age: number //在TypeScript中,类的属性必须有初始值,可以在等号后面赋值或者在构造函数中初始化 constructor (name: string, age: number) { this.name = name // 直接通过this访问当前类的属性会报错 this.age = age } sayHi (msg: string): void { // ES6的语法为类型声明方法 console.log(`I am ${this.name}, ${msg}`) } }以函数中为例,泛型就是在声明函数时,不去制定具体类型,等到调用的时候再去传递一个具体类型,其目的是为了极大程度的复用代码。
function createNumberArray (length: number, value: number): number[] { // Array默认any类型 使用泛型参数传递类型 这里的Array就是一个泛型类 // 在TypeScript内部去定义Array类型的时候并不知道我们使用其存放什么样的数据 // 使用泛型参数 在我们调用时再去传递一个具体的类型 const arr = Array<number>(length).fill(value) // ES6 fill()填充 return arr } const res = createNumberArray(3, 100) // res => [100, 100, 100] 使用泛型把类型变成参数,在我们调用的时候再去传递这个类型 function createArray<T> (length: number, value: T): T[] { const arr = Array<T>(length).fill(value) return arr } const res = createArray<string>(3, 'foo') 泛型就是把我们定义时不能确定的类型变成一个参数,让我们去使用时再去传递类型参数拿lodash举例
//把字符串转化为驼峰格式 参数和返回值为string import { camelCase } from 'lodash'当我们直接去调用函数的时候发现并没有看到任何类型提示
const res = camelCase('hello typed')这种情况下需要单独的类型声明
declare function camelCase (input: string): string有了这样一个声明过后再去使用这个函数时就会有对应的类型限制
目前绝大多数哦npm模块都已经提供了对应的声明,只需要安装对应的声明模块即可拿lodash举例,我们import时报错会发现,会建议我们去安装一个@type/lodash的模块,这个就是lodash对应的类型声明模块,需要注意的是:类型声明模块应该是一个开发模块,它里面不会提供任何的具体的代码,只是对一个模块做对应的类型声明。
目前越来越多的模块已经在内部集成了这种类型声明文件