初识 TypeScript 是由巨硬开发的 JS 的超集 ,通过添加可选的静态类型和基于类的面向对象编程思想,使得代码更易于理解、维护和重构。此外,TS 还支持其他高级语言功能,如接口、泛型、命名空间和装饰器等
规范: TS 提供了更好的类型检查和代码提示工具,并且可以使用第三方库中的类型声明文件进行代码补全和类型检查
开发: TS 为现代Web应用程序和大型项目提供了一个强大的编程环境,可以帮助开发人员减少错误、提高开发效率和代码质量
运行: TS 可以转换成 JS 并在任何支持 JS 的环境中运行,包括浏览器、Node.js和移动应用程序等
安装: npm install typescript -g
会在全局暴露一个 tsc 命令
tsc -init
初始化,创建 tsconfig.json
tsc -w
监视所有 TS 文件变化,实时编译成 JS
npm install ts-node -g
安装 ts-node 更方便地运行 TS
配置文件 tsconfig.json 是 TS 的配置文件
可以定义哪些文件需要包含在编译过程中、使用哪个ECMAScript目标版本、生成哪种模块系统、是否开启严格模式等等
常用配置项:
target 设置编译后的 JavaScript 代码使用的 ECMAScript 版本,默认为 ES3,可设置为 ES5、ES6/ES2015、ES7 等。
strict 开启所有严格类型检查选项,包括noImplicitAny,noImplicitThis,alwaysStrict等等。如果希望 TypeScript 给出尽可能多的类型检查错误提示,可以将此选项设置为 true。
module 指定生成的模块规范类型,可选值有 commonjs、amd、system 和 es2015。
lib 指定编译过程中需要引入的库文件,默认情况下只包含 DOM 和 ES 标准库,可以通过指定其他库来获得更好的类型检查支持。
moduleResolution 指定模块解析策略,可选值有 classic、node、和 yarn pnp。
esModuleInterop 简化导入cjs模块功能
outDir 输出目录,指定编译后的 JavaScript 代码所在的目录。
rootDir 项目根目录
allowJs 允许编译器编译 .js 文件(通常是为了容易迁移现有的 JavaScript 项目到 TypeScript 项目)
sourceMap 生成 sourcemap 文件,方便调试 TypeScript 代码时能够正确地映射回原始的 TypeScript 代码。
noImplicitAny 当 TypeScript 无法推导出变量的类型时,给出一个错误提醒
noImplicitThis 禁止 this 关键字的隐式 any 类型
strictNullChecks 严格模式下对 null 和 undefined 的检查。
TS检查相关注释 (1) // @ts-nocheck
加到文件首行,当前文件不需要 ts 校验 (2) // @ts-check
加到文件首行,对当前文件进行 ts 校验 (3) // @ts-ignore
忽略下一行代码的 ts 校验 (4) // eslint-disable-next-line
忽略下一行代码的 eslint 校验 (5) /* eslint-disable */
eslint忽略
类型标注 通过 <数据>:<类型>
对变量、函数返回值、函数参数等数据进行类型的标注、限制
思想: 定义任何东西的时候要注明类型,调用任何东西的时候要检查类型。
可以标注的类型: (1) 基础类型: Boolean、Number、String、null、undefined 以及 ES6 的 Symbol 和 ES10 的 BigInt。 (2) 空值 :Void (3) 顶级类型: 任意类型 Any 和 不知道的类型 Unknown (4) Object、object 和 {} (5) 接口和对象类型: interface定义 (6) 数组类型 (7) 函数类型
基本类型 1、字符串类型:string
1 2 3 let str : string = "chuckle" ;str = `1+1=${1 +1 } ` ; str = "1+1=" + "2" ;
2、数字类型:number
1 2 3 4 5 let num : number = 1 num = NaN num = Infinity num = 0xf00d num = 0b1010
3、布尔类型:boolean
注意: new Boolean() 返回的是一个 Boolean 对象,而不是布尔值
1 2 let bool : boolean = true bool = Boolean (0 )
4、Null 和 undefined 类型
undefined 和 null 是所有类型的子类型 ,在非严格模式 下可以赋给其它任何类型 的变量
严格模式下: (1) undefined 和 null 不能赋给其它类型的变量,但 undefined 可以赋给 void 类型(一般也用不到) (2) null 和 undefined 类型也不能相互赋值
1 2 3 4 5 6 let a : null = null let b : undefined = undefined a = b let str : string = a let c : void = null c = undefined
空值void JS 中没有空值的概念,TS 中可以使用 void 表示函数无返回值
注意: (1) 不能将 void 赋给除 any 外其它类型的变量。 (2) 使用 any 类型的变量接收函数返回的空值,打印 undefined (3) Boolean(void) 值是 false
1 2 3 4 5 6 function fun ( ): void {}let a : any = fun ();console .log (a); a = Boolean (fun ()); console .log (a); let b : string = fun ()
any和unknown any 和 unknown 是 TS 中的顶级类型,可以包含所有类型的数据
如果所有变量都是 any 类型,那就是 AnyScript,写起来和 JS 没什么区别,最好不要这么做
1 2 3 4 let a : any = 1 ;a = "qx" a = true a = null
any 与 unknown 的区别: any 可以赋给其它类型,而 unknown 不能赋值给除 any 和 unknown 以外的其它类型
1 2 3 4 5 let a : any = 1 let b : unknown = 2 let num : number = a a = b num = b
unknown 类型不能读任何元素、属性,也不能调用任何方法,所以 unknown 比 any 安全
1 2 3 4 5 let u : unknown = { a : 1 }console .log (u); console .log (u.a ); u = ()=> { console .log ('@@' ) } u ()
Object、object和{} Object、object 和 {} 虽然能保存的数据类型不同,但都和 unknown 类型一样,不能读任何元素、属性,也不能调用任何方法
1、 Object 类型是所有 Object 类的实例的类型,字面量 {} 代表的也是 Object
由于原型链顶层就是 Object,所有基本数据类型和引用类型最终都指向 Object,所以他也包含所有类型
1 2 3 4 5 let obj : Object = 1 ;obj = "chuckle" console .log (obj); obj = { a : 1 } console .log (obj.a );
2、 object 代表所有非值类型 的类型(数组、对象、函数)等,常用于泛型约束
1 2 3 4 5 6 let o : object = ()=> { console .log ('@@' ) }o () o = ['1' ,'2' ] console .log (o[1 ]) o = { a : 1 } console .log (o)
接口和对象类型 在 TypeScript 中,使用接口(Interfaces)来定义对象的类型。
即使用 interface 来定义一种对对象的约束
接口是一个非常灵活的概念,除了可用于对类的一部分行为 进行抽象外,也常用于对象的形状 的描述。
1 2 3 4 5 6 7 8 9 10 11 interface Person { name : String , age : number , } let person : Person = { name : "chuckle" , age : 20 }
1、重名的接口会合并,共同起作用
1 2 3 4 5 6 7 8 9 10 11 interface Person { name : String , } interface Person { age : number , } let person : Person = { name : "chuckle" , age : 20 }
2、任意属性 [propName: string] (索引签名)
声明的对象中可以有任意个数所定义的类型(或其子类型)的属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 interface Person { name : String , age : number , [propName : string ]: any } let person : Person = { name : "chuckle" , age : 20 , a : 1 , b : "qx" , c ( ){ console .log (this .a ) } }
3、readonly 让属性只读
1 2 3 4 interface Person { readonly name : String , age : number , }
4、定义对象中的函数
1 2 3 interface Person { fun : (value:number )=> number }
6、接口继承 extends
1 2 3 4 5 6 7 8 9 10 11 interface A{ name : String , } interface B extends A{ age : number , } let b : B = { name : "chuckle" , age : 20 , }
数组类型 使用 :<元素类型>[]
来定义一个数组
使用泛型的方式定义
定义二维数组
1 2 let arr : number [][]let arr : Array <Array <number >>
定义包含多种类型的数组
1 let arr : (number |string )[] = [1 , 2 , '3' , 4 ]
元组 元组:赋值时,元素的类型、位置、个数必须一一对应
1 let arr : [number ,string ] = [1 ,'qx' ]
允许起名加上可选修饰符,对于越界的元素他的类型被限制为联合类型,也就是可以push已有的元素类型
1 2 3 4 const arr : [x : number , y?: boolean ] = [1 , true ]arr.push (1 ) arr.push ("1" ) console .log (arr)
可以用于表格数据的约束
1 2 3 4 5 let excel : [name : string, sex : string, age : number][] = [ ['张三' , '男' , 18 ], ['张三' , '男' , 18 ], ['张三' , '男' , 18 ] ]
函数类型与定义 定义一个函数类型变量
1 2 let fun : Function fun = ()=> {}
定义函数的参数和返回值
1 2 3 4 function fun (a:number , b:number ):number { return a+b; } console .log (fun (1 ,1 ))
使用 interface 定义函数类型(函数也是一种对象)
1 2 3 4 5 6 7 8 9 10 interface Fn { (name : string ): number } let fun : Fn = (name )=> { console .log (name) return 1 } fun ("chuckle" )
TS 可以定义函数中的 this 类型,用于增强补全,必须写在函数的第一个参数上,不作为真正的参数定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 interface Obj { name : string add : (this :Obj, num:number )=> void } let obj :Obj = { name : "chuckle" , add (num:number ){ console .log (num); console .log (this .name ); } } obj.add (1 );
?可选符 使用 ?:
定义函数参数或对象属性作为可选项
1 2 3 4 5 6 7 8 9 interface Person { name : String , age?: number , } function fun (a:number , b?:number ):number { return b ? a+b : a; }
联合类型 使用 <类型1>|<类型2>
标注多个类型
1 2 3 4 function fun (a: number | string ) { }let a : number | string
交叉类型 <类型1>&<类型2>
需同时满足两种类型,通常配合接口使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 interface Student { name : string id : number } interface Person { name : string age : number } let p : Student & Person = { name : "chuckle" , id : 1 , age : 20 }
类型断言 值 as 类型
或<类型>值
欺骗 TypeScript 编译器,但无法避免运行时的错误
1 2 3 4 5 6 function fun (a: number | string ){ console .log ((<string >a).length ); } fun (123 ) fun ('123' )
TS 直接在 window 上添加属性是不允许的,但可以将 window 断言为 any 类型,就可以添加属性了
1 2 3 window .a = 1 ; (<any >window ).a = 1 ; console .log ((<any >window ).a );
内置对象 JS 中有很多内置对象,它们可以直接在 TS 中当做定义好了的类型来使用
1、ECMAScript 的内置对象: 内置对象名就是类型名
1 2 3 4 5 6 7 8 9 10 11 12 13 let b : Boolean = new Boolean (1 )console .log (b)let n : Number = new Number (true )console .log (n)let s : String = new String ('chuckle' )console .log (s)let d : Date = new Date ()console .log (d)let r : RegExp = /^123/ console .log (r)let e : Error = new Error ("error" )console .log (e)
2、DOM 和 BOM 的内置对象: HTML<标签名>Element
某个内置标签的类型,input、span 等HTMLElement
语义化的标签名,footer、header,以及自定义标签名Element
任何标签元素的类型NodeList
任何元素集合的类型NodeListOf<其它类型>
指定元素集合的类型
由于元素可能获取不到,要加上 null 组成联合类型
1 2 3 4 5 6 let div1 : HTMLInputElement | null = document .querySelector ('input' );let div2 : HTMLDivElement | null = document .querySelector ('div' );let div3 : NodeListOf <HTMLElement > | null = document .querySelectorAll ('.content' );let local : string | null = localStorage .getItem ('token' )let lct : Location = locationlet pms : Promise <number > = new Promise ((r )=> r (1 ))
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 interface HTMLElementTagNameMap { "a" : HTMLAnchorElement ; "abbr" : HTMLElement ; "address" : HTMLElement ; "applet" : HTMLAppletElement ; "area" : HTMLAreaElement ; "article" : HTMLElement ; "aside" : HTMLElement ; "audio" : HTMLAudioElement ; "b" : HTMLElement ; "base" : HTMLBaseElement ; "bdi" : HTMLElement ; "bdo" : HTMLElement ; "blockquote" : HTMLQuoteElement ; "body" : HTMLBodyElement ; "br" : HTMLBRElement ; "button" : HTMLButtonElement ; "canvas" : HTMLCanvasElement ; "caption" : HTMLTableCaptionElement ; "cite" : HTMLElement ; "code" : HTMLElement ; "col" : HTMLTableColElement ; "colgroup" : HTMLTableColElement ; "data" : HTMLDataElement ; "datalist" : HTMLDataListElement ; "dd" : HTMLElement ; "del" : HTMLModElement ; "details" : HTMLDetailsElement ; "dfn" : HTMLElement ; "dialog" : HTMLDialogElement ; "dir" : HTMLDirectoryElement ; "div" : HTMLDivElement ; "dl" : HTMLDListElement ; "dt" : HTMLElement ; "em" : HTMLElement ; "embed" : HTMLEmbedElement ; "fieldset" : HTMLFieldSetElement ; "figcaption" : HTMLElement ; "figure" : HTMLElement ; "font" : HTMLFontElement ; "footer" : HTMLElement ; "form" : HTMLFormElement ; "frame" : HTMLFrameElement ; "frameset" : HTMLFrameSetElement ; "h1" : HTMLHeadingElement ; "h2" : HTMLHeadingElement ; "h3" : HTMLHeadingElement ; "h4" : HTMLHeadingElement ; "h5" : HTMLHeadingElement ; "h6" : HTMLHeadingElement ; "head" : HTMLHeadElement ; "header" : HTMLElement ; "hgroup" : HTMLElement ; "hr" : HTMLHRElement ; "html" : HTMLHtmlElement ; "i" : HTMLElement ; "iframe" : HTMLIFrameElement ; "img" : HTMLImageElement ; "input" : HTMLInputElement ; "ins" : HTMLModElement ; "kbd" : HTMLElement ; "label" : HTMLLabelElement ; "legend" : HTMLLegendElement ; "li" : HTMLLIElement ; "link" : HTMLLinkElement ; "main" : HTMLElement ; "map" : HTMLMapElement ; "mark" : HTMLElement ; "marquee" : HTMLMarqueeElement ; "menu" : HTMLMenuElement ; "meta" : HTMLMetaElement ; "meter" : HTMLMeterElement ; "nav" : HTMLElement ; "noscript" : HTMLElement ; "object" : HTMLObjectElement ; "ol" : HTMLOListElement ; "optgroup" : HTMLOptGroupElement ; "option" : HTMLOptionElement ; "output" : HTMLOutputElement ; "p" : HTMLParagraphElement ; "param" : HTMLParamElement ; "picture" : HTMLPictureElement ; "pre" : HTMLPreElement ; "progress" : HTMLProgressElement ; "q" : HTMLQuoteElement ; "rp" : HTMLElement ; "rt" : HTMLElement ; "ruby" : HTMLElement ; "s" : HTMLElement ; "samp" : HTMLElement ; "script" : HTMLScriptElement ; "section" : HTMLElement ; "select" : HTMLSelectElement ; "slot" : HTMLSlotElement ; "small" : HTMLElement ; "source" : HTMLSourceElement ; "span" : HTMLSpanElement ; "strong" : HTMLElement ; "style" : HTMLStyleElement ; "sub" : HTMLElement ; "summary" : HTMLElement ; "sup" : HTMLElement ; "table" : HTMLTableElement ; "tbody" : HTMLTableSectionElement ; "td" : HTMLTableDataCellElement ; "template" : HTMLTemplateElement ; "textarea" : HTMLTextAreaElement ; "tfoot" : HTMLTableSectionElement ; "th" : HTMLTableHeaderCellElement ; "thead" : HTMLTableSectionElement ; "time" : HTMLTimeElement ; "title" : HTMLTitleElement ; "tr" : HTMLTableRowElement ; "track" : HTMLTrackElement ; "u" : HTMLElement ; "ul" : HTMLUListElement ; "var" : HTMLElement ; "video" : HTMLVideoElement ; "wbr" : HTMLElement ; }
代码雨 1 2 3 4 5 6 7 8 *{ padding : 0 ; margin : 0 ; overflow : hidden; } body { background-color : #000 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import './index.css' let canvas : HTMLCanvasElement = document .querySelector ('#canvas' );let ctx = canvas.getContext ('2d' );canvas.width = screen.availWidth ; canvas.height = screen.availHeight ; let str : string [] = 'qcqxchuckle0101010101' .split ('' );let arr = Array (Math .ceil (canvas.width / 10 )).fill (0 );const rain = ( ) => { ctx.fillStyle = 'rgba(0,0,0,0.05)' ; ctx.fillRect (0 , 0 , canvas.width , canvas.height ); ctx.fillStyle = '#0f0' ; arr.forEach ((item, index ) => { if (Math .random () > 0.2 ) { const text = str[Math .floor (Math .random () * str.length )]; ctx.fillText (text, index * 10 , item); } arr[index] = item > canvas.height || item > Math .random () * canvas.height * (canvas.height > 1000 ? 20 : 10 ) ? 0 : item + 10 }); }; let stv = setInterval (rain, 40 );window .addEventListener ('resize' , () => { clearInterval (stv); canvas.width = screen.availWidth ; canvas.height = screen.availHeight ; stv = setInterval (rain, 40 ); });
Class Class类是ES6新特性,详见:ES6-Class类
在TS中使用interface 定义类,当然interface还能定义对象,因为对象是类的实例,类是对象的模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 interface Options { el : string | HTMLElement } interface VueCls { options : Options init (): void } class Vue implements VueCls { options : Options constructor (options: Options ) { this .options = options this .init () } init (): void { console .log (this .options ) } } const vue = new Vue ({ el : '#app' });
一个类可以由多个interface进行约束
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 interface VueCls1 { options : Options } interface VueCls2 { init (): void } class Vue implements VueCls1 , VueCls2 { options : Options constructor (options: Options ) { this .options = options this .init () } init (): void { console .log (this .options ) } }
继承 Class 通过 extends
关键字实现继承,详见:ES6-类的继承
一个简化版的虚拟DOM案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 interface Options { el : string | HTMLElement } interface VueCls { options : Options init (): void } interface Vnode { tag : string text?: string children?: Vnode [] } class Dom { createElement (tag: string ) { return document .createElement (tag); } setText (el: HTMLElement, value: string ) { el.textContent = value; } render (data: Vnode ) { const root = this .createElement (data.tag ); if (data.text ) { this .setText (root, data.text ) } if (data.children && Array .isArray (data.children )) { data.children .forEach (child => { const e = this .render (child) root.appendChild (e); }) } return root; } } const data : Vnode = { tag : 'div' , text : "这是一个div标签" , children : [ { tag : 'p' , text : "这是一个p标签" }, { tag : 'div' , children : [ { tag : 'p' , text : "这是一个p标签" }, { tag : 'span' , text : "这是一个span标签" }, ] }, ], } class Vue extends Dom implements VueCls { options : Options constructor (options: Options ) { super (); this .options = options this .init () } init (): void { const dom = this .render (data) const app = typeof this .options .el === "string" ? document .querySelector (this .options .el ) : this .options .el app?.appendChild (dom) } } const vue = new Vue ({ el : '#app' });
渲染结果:
1 2 3 4 5 6 7 8 9 <div id ="app" > <div > 这是一个div标签 <p > 这是一个p标签</p > <div > <p > 这是一个p标签</p > <span > 这是一个span标签</span > </div > </div > </div >
访问修饰符 访问修饰符是TS提供的特性,总共有三个:public private protected ,用于控制对类内部成员的访问权限
public
默认,实例和子类都能访问
private
私有的,只能在类内部访问,实例和子类都不能访问
protected
类和子类能访问,实例不能访问
一个特殊的:readonly
只能访问,不能修改(可以在constructor中初始化)
对上面的虚拟dom案例进行修饰:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 interface Options { el : string | HTMLElement } interface VueCls { options : Options init (): void } interface Vnode { tag : string text?: string children?: Vnode [] } class Dom { private createElement (tag: string ) { } private setText (el: HTMLElement, value: string ) { } protected render (data: Vnode ) { } } class Vue extends Dom implements VueCls { readonly options : Options constructor (options: Options ) { super (); } init (): void { } }
注意: 接口中不能出现 public, protected, private 修饰符,因为默认只能规定类中的 public 属性和方法,也就是说接口中定义的成员都是 public 的
1 2 3 4 5 6 7 8 9 10 11 interface VueCls { init (): void } class Vue extends Dom implements VueCls { private init (): void { } }
抽象类 abstract
关键字定义抽象类和抽象方法,抽象类无法实例化,抽象方法只能描述
作用:用于顶层设计,派生类实现功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 abstract class Person { name : string constructor (name: string ) { this .name = name } abstract setName (name : string ): void } class Student extends Person { setName (name : string ): void { this .name = name } } const stu = new Student ("chuckle" )console .log (stu.name ) stu.setName ("qcqx" ) console .log (stu.name )
枚举类型 enum
定义枚举类型,枚举成员只能是数字或字符串,且readonly
枚举成员也可以被当做一个类型,可以指定某些变量的值必须是枚举成员的值
聊聊TypeScript中枚举对象(Enum)
1、数字枚举 默认从0开始枚举,可以反向映射
1 2 3 4 5 6 7 8 9 enum Color { red, green, blue } console .log (Color )console .log (Color [0 ]) console .log (Color .red )
可以指定初值
1 2 3 4 5 6 7 8 9 enum Color { red = 1 , green, blue } console .log (Color )console .log (Color [0 ]) console .log (Color .red )
2、字符串枚举 每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化
1 2 3 4 5 6 7 8 enum Color { red = 'red' , green = 'green' , blue = 'blue' } console .log (Color )console .log (Color .red )
3、异构枚举 枚举可以混合字符串和数字成员
1 2 3 4 5 6 7 enum Color { red = 'red' , green = 'green' , blue = 3 } console .log (Color )
4、接口枚举 用于约束成员值的范围
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 enum Color { red = 'red' , green = 'green' , blue = 3 } interface A { red : Color .red ; blue : Color .blue ; } let obj : A = { red : Color .red , blue : 3 }
4、const枚举 直接将枚举编译为常量,而普通声明的枚举编译完后是个对象,可以避免在额外生成的代码上的开销和额外的非直接的对枚举成员的访问
注意:const枚举不能反向映射
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const enum Types { No = "No" , Yes = 1 , } console .log (Types .No )console .log (Types .Yes )console .log ("No" );console .log (1 );var Types ;(function (Types ) { Types ["No" ] = "No" ; Types [Types ["Yes" ] = 1 ] = "Yes" ; })(Types || (Types = {})); console .log (Types .No );console .log (Types .Yes );
类型推论 TS会在没有明确的指定类型的时候推断出一个类型
1 2 3 4 let str = "chuckle" str = 123
若变量没有初始化,会在首次赋值时推断类型
1 2 3 let strstr = "qx" str = 123
类型别名 type
定义类型别名,多用于复合类型
1 2 3 4 5 6 7 8 9 10 11 type t1 = number | string let s : t1type t2 = (name: string ) => string const fn1 : t2 = (name ) => { return name } type value = boolean | 0 | '123' let a : value = 0
type 和 interface 区别:
interface可以继承 type 只能通过 & 交叉类型合并
type 可以定义联合类型、可以使用一些操作符 interface 不行
interface 遇到重名的会合并 type 不行
type 中的 extends 表示包含,判断左值是否为右类型的子类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 type num1 = 1 extends number ? 1 : 0 type num2 = 1 extends string ? 1 : 0 type num3 = {} extends number ? 1 : 0 type num5 = 1 extends Object ? 1 : 0 type num6 = (()=> {}) extends object ? 1 : 0 type num7 = Number extends object ? 1 : 0 type num8 = number extends object ? 1 : 0 type num9 = Object extends object ? 1 : 0 type num10 = any extends object ? 1 : 0 type num11 = void extends Object | string | object ? 1 : 0 type num12 = void extends null | undefined ? 1 : 0 type num13 = null | undefined extends object ? 1 : 0 type num14 = null extends undefined ? 1 : 0 type num15 = undefined extends null ? 1 : 0 type num16 = undefined | null extends never ? 1 : 0 type num17 = never extends undefined & null ? 1 : 0
类型的层级:
1 2 3 4 5 6 object 包含所有非值类型(undefined,null除外) any unknown Object Number String Boolean number string boolean never
never类型 never 类型来表示不应该存在的状态
1 2 3 4 5 6 7 8 9 10 type A = void | number | never type B = number & string function error (message: string ): never { throw new Error (message); } function loop ( ): never { while (true ) {} }
作用:便于TS检查一些可能存在的运行时的期望错误
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 type Value = 1 | 2 | 3 function choice (value: Value ) { switch (value) { case 1 : break case 2 : break case 3 : break default : const error : never = value; return error } }
泛型 泛型是动态类型,即在使用时确定类型,相比于暴力使用any,泛型即保留了方便的类型检查又增加了灵活性
1 2 3 4 5 function toArray<T>(a : T, b : T): Array <T> { return [a, b] } toArray<number >(1 , 2 ) toArray (1 , 2 )
泛型有些类似类型的函数,先用个T作为形式类型占位,再由外部传入实际类型,或者让TS自动推断,所以<>中可以用多个形式类型占位
1 2 3 4 5 function toArray<T, K>(a : T, b : K): Array <T | K> { return [a, b] } toArray<number , string >(1 , "a" ) toArray (1 , "a" )
除了在函数中,interface、type、class 等都可以使用泛型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 type AA <T> = number | string | Tlet aa : AA <boolean > aa = false aa = 1 aa = "aa" interface Data <T> { msg : T } let d : Data <string > = { msg : "hello" }class Sub <T>{ attr : T[] = []; add (a : T): T[] { this .attr .push (a) return this .attr } } let ss = new Sub <number >()ss.attr = [1 , 2 , 3 ] ss.add (123 ) let sss = new Sub <string >()sss.attr = ['1' , '2' , '3' ] sss.add ('123' )
一个常用的技巧: 因为 js 中的 number/string/boolean 这些基本量是传值的,而 object/array 这些对象传引用地址,当我们想在函数内部通过参数修改传进来的数据时,就得使用引用数据类型,所以我们把基本量包装成一个对象传进去,就可以实现在函数内部修改参数了
1 2 3 type ref<T> = { value : T }
泛型在封装请求方法时也经常使用:
data.json 1 2 3 4 5 6 7 8 { "code" : "0000" , "msg" : "成功" , "data" : { "name" : "qx" , "age" : 20 } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const axios = { get<T>(url : string ): Promise <T> { return new Promise <T>((resolve, reject ) => { let xhr = new XMLHttpRequest () xhr.open ('get' , url) xhr.onreadystatechange = () => { if (xhr.readyState === 4 && xhr.status === 200 ) { resolve (JSON .parse (xhr.responseText )) } } xhr.send (null ) }) } } interface ResData { code : string msg : string data : any } axios.get <ResData >('./data.json' ).then (res => { console .log (res.data ) })
泛型约束keyof 在<>中使用extends约束泛型的类型范围
1 2 3 4 5 6 7 8 9 interface Len { length : number } function getLegnth<T extends Len >(arg : T) { return arg.length }
keyof
可以获取一个对象类型的所有键,作为联合类型
1 2 3 4 5 6 7 8 9 function prop<T, K extends keyof T>(obj : T, key : K) { return obj[key] } let o = { a : 1 , b : 2 , c : 3 }prop (o, 'a' )
更灵活的用法:
1 2 3 4 5 6 7 8 9 10 11 12 interface Info { name : string age : number } type InfoOptions <T extends object > = { [Key in keyof T]?: T[Key ] } type BB = InfoOptions <Info >
tsconfig.json tsconfig.json
是TS的配置文件,通过 tsc --init
生成
配置详解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 { "compilerOptions" : { "incremental" : true , "tsBuildInfoFile" : "./buildFile" , "diagnostics" : true , "target" : "ES5" , "module" : "CommonJS" , "lib" : [ "DOM" , "ES5" , "ES6" , ] , "allowJs" : true , "checkJs" : true , "outDir" : "./dist" , "rootDir" : "./" , "declaration" : true , "declarationDir" : "./file" , "sourceMap" : true , "declarationMap" : true , "typeRoots" : [ ] , "types" : [ ] , "removeComments" : true , "noEmit" : true , "noEmitOnError" : true , "noEmitHelpers" : true , "importHelpers" : true , "downlevelIteration" : true , "strict" : true , "alwaysStrict" : true , "noImplicitAny" : true , "strictNullChecks" : true , "strictFunctionTypes" : true , "strictPropertyInitialization" : true , "strictBindCallApply" : true , "noImplicitThis" : true , "noUnusedLocals" : true , "noUnusedParameters" : true , "noFallthroughCasesInSwitch" : true , "noImplicitReturns" : true , "esModuleInterop" : true , "allowUmdGlobalAccess" : true , "moduleResolution" : "node" , "baseUrl" : "./" , "paths" : { "@/*" : [ "./" ] } , "rootDirs" : [ "src" , "out" ] , "listEmittedFiles" : true , "listFiles" : true } , }
命名空间 namespace
定义命名空间,内部成员默认私有,通过 export
暴露
作用: 组织代码,避免命名冲突
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 namespace A { export const B = 123 ; const C = 456 ; export namespace AA { export const BB = B + C; } } console .log (A.B ); console .log (A.AA .BB ); namespace A { export const D = 678 ; } console .log (A.D ); import X = A.AA ;console .log (X.BB );
三斜线指令 三斜线指令 是包含单个XML标签的单行注释。注释的内容会做为编译器指令使用。
TS3.0后建议用import
<reference path="..." />
类似import,导入依赖,但无需export先暴露
1 2 3 console .log (X1 .A , X2 .A );
<reference types="node" />
声明文件引入,表明这个文件使用了 @types/node/index.d.ts
里面声明的名字; 并且,这个包需要在编译阶段与声明文件一起被包含进来。
声明文件d.ts .d.ts
(declare),TS的声明文件。对于第三方纯js库,通过d.ts可以获得完整的TS静态类型检查、提示、补全等功能
1 2 3 4 5 6 7 declare var declare function declare class declare enum declare namespace // 声明(含有子属性的)全局对象interface 和 type // 声明全局类型 /// <reference /> 三斜线指令
TS会解析项目中所有的 *.ts 文件,当然也包含 .d.ts,注意 files、include 和 exclude 配置
TS2.0后,还会默认的查看 node_modules/@types
文件夹,用于为 TypeScript 提供有关用 JavaScript 编写的 API(第三方库) 的类型信息
热门的JS第三方库通常社区已经维护了其声明文件,安装:npm i @types/<包名> -D
部分库如axios,已经在package.json中指定了声明文件
1 2 3 { "types" : "index.d.ts" , }
对于社区没有维护声明文件的第三方库,就需要通过declare module
语法自己写了,declare关键字
以express为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import express from 'express' const app = express ()const router = express.Router ()app.use ('/api' , router) router.get ('/list' , (req, res ) => { res.json ({ code : 200 }) }) app.listen (9001 ,()=> { console .log (9001 ) }) declare module 'express' { interface Router { get (path : string , cb : (req: any , res: any ) => void ): void } interface App { use (path : string , router : any ): void listen (port : number , cb?: () => void ): void } interface Express { (): App Router (): Router } const express : Express export default express }
也可以为项目中常用的类型编写一个声明文件,声明语句中只能定义类型,不能在声明语句中定义具体的实现
1 2 3 4 const num : ref<number > = { value : 0 , } x = 1 ;
index.d.ts 1 2 3 4 declare type ref<T> = { value : T } declare let x :number ;
Mixins混入 对象的混入:Object.assign
合并多个对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 interface Name { name : string } interface Age { age : number } interface Sex { sex : number } let people1 : Name = { name : "chuckle" }let people2 : Age = { age : 20 }let people3 : Sex = { sex : 1 } const people = Object .assign (people1,people2,people3)
类方法的混入:
TS并没有Mixins关键字,实现类的混入需要用赋值断言、写混入函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class O1 { name!: string ; getName (): void { console .log (this .name ); } } class O2 { age!: number ; getAge (): void { console .log (this .age ); } } class O3 implements O1 , O2 { name : string = 'chuckle' ; getName!: () => void age : number = 20 ; getAge!: () => void } applyMixins (O3 , [O1 , O2 ]);let smartObj = new O3 ();smartObj.getName (); smartObj.getAge (); function applyMixins (derivedCtor: any , baseCtors: any [] ) { baseCtors.forEach (baseCtor => { console .log (baseCtor.prototype ); Object .getOwnPropertyNames (baseCtor.prototype ).forEach (name => { derivedCtor.prototype [name] = baseCtor.prototype [name]; }); }); }
相较于继承,这样混入使得只有类方法被“继承”
装饰器 装饰器就是一个函数,可以注入到类、方法、属性、参数,对象上,扩展其功能,增加了代码的可读性,清晰地表达了意图。
这是一个实验性的功能,虽然TS5.0 后已经正式推出
tsconfig.json 1 2 "experimentalDecorators" : true ,
类装饰器 类装饰器ClassDecorator
应用于类构造函数,可以用来监视,修改或替换类定义
1 2 3 type ClassDecorator = <TFunction extends Function >( target: TFunction ) => TFunction | void ;
通过装饰器给类添加属性和方法:
1 2 3 4 5 6 7 8 9 10 11 12 const Base : ClassDecorator = (target: Function ) => { console .log (target); target.prototype .fn = () => { console .log ("chuckle" ); } } @Base class CLS { }const cls = new CLS () as any ;cls.fn ()
返回值 如果类装饰器返回一个值,它会使用提供的构造函数(类)来替换类的声明。并且必须保证能覆盖原来类的属性和方法,所以通常继承原来的类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function classDecorator<T extends { new (...args : any []): {} }>(constructor : T) { return class extends constructor { newProperty = "new property" ; hello = "override" ; } } @classDecorator class Greeter { property = "property" ; hello : string ; constructor (m: string ) { this .hello = m; } } const greeter = new Greeter ("world" );console .log (greeter);
注意,应该返回一个匿名类,否则实例前面的类型提示是返回的类名,而不是原来的类名,这容易造成误解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 let a : any = null ;function classDecorator<T extends { new (...args : any[]): {} }>(constructor : T) { class A extends constructor { newProperty = "new property" ; hello = "override" ; } a = A; return A } @classDecorator class Greeter { property = "property" ; hello : string; constructor (m: string ) { this .hello = m; } } const greeter = new Greeter ("world" );console .log (greeter);console .log (a === Greeter );
装饰器工厂 装饰器工厂是一个高阶函数(函数柯里化),外层的函数接受值,里层的函数最终接受类的构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const Base = (name: string ) => { const fn : ClassDecorator = (target: Function ) => { console .log (target); target.prototype .name = name; target.prototype .fn = () => { console .log (name); } } return fn; } @Base ("chuckle" )class CLS { constructor ( ) { } } const cls = new CLS () as any ;cls.fn ()
方法装饰器 方法装饰器MethodDecorator
应用到方法的属性描述符上,用来监视,修改或者替换方法定义
1 2 3 4 5 type MethodDecorator = <T>( target: Object , propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T> ) => TypedPropertyDescriptor <T> | void ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const met :MethodDecorator = (...args ) => { console .log (args); } class A { @met getName ():string { return 'chuckle' } } const a = new A ();
实现对原方法的拦截加工:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 function MethodInterceptor (params?: string ): MethodDecorator { return (target: Object , name: string | symbol, decr: PropertyDescriptor ) => { let temp = decr.value ; decr.value = function (...args: any ) { console .log ('前置拦截' ); console .log (this ); temp.call (this , ...args) console .log ('后置拦截' ); } } } class CLS { name = 'chuckle' @MethodInterceptor ("chuckle" ) fn (...args: any ) { console .log (args); } } let cls = new CLS ()cls.fn (1 , 2 , 3 )
实现GET装饰器 请求通过装饰器工厂传入的url,将返回的数据传给原方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 function GET (url: string ): MethodDecorator { return (target: Object , name: string | symbol, decr: PropertyDescriptor ) => { const fnc = decr.value ; fetch (url).then (res => res.json ()) .then (data => { fnc ({ code : 200 , msg : 'success' , data }) }).catch (err => { fnc ({ code : 500 , msg : err, data : null }) }) } } class CLS { @GET ('https://api.github.com/' ) fn (result: any ) { console .log (result.code ); console .log (result.msg ); console .log (result.data ); } } const cls = new CLS ();
参数装饰器 参数装饰器应用于类构造函数或方法声明,没有返回值。用来监视一个方法的参数是否被传入。
1 2 3 4 5 type ParameterDecorator = ( target: Object , propertyKey: string | symbol | undefined , parameterIndex: number ) => void ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const currency : ParameterDecorator = ( target: Object , key: string | symbol | undefined , index: number ) => { console .log (target === CLS .prototype ) console .log (target, key, index) } class CLS { name = 'chuckle' setName (@currency name: string ) { this .name = name } }
属性装饰器 属性装饰器应用较少,用来监视类中是否声明了某个名字的属性。
1 2 3 4 type PropertyDecorator = ( target: Object , propertyKey: string | symbol ) => void ;
1 2 3 4 5 6 7 8 const met : PropertyDecorator = (target: Object , key: string | symbol ) => { console .log (target, key); } class CLS { @met name = 'chuckle' }
顺序 多个同类型装饰器可以同时应用到一个声明上
求值方式与复合函数相似。复合的结果(f ∘ g)(x)等同于f(g(x))。
当多个装饰器应用在一个声明上时会进行如下步骤的操作:
由上至下依次对装饰器表达式求值。
求值的结果会被当作函数,由下至上依次调用(应用)。
装饰器求值 类中不同声明上的装饰器将按以下规定的顺序应用:
参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个实例成员。
参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个静态成员。
参数装饰器应用到构造函数。
类装饰器应用到类。
方法参数装饰器执行的优先级最高,而方法和属性装饰器受到定义顺序的影响。类与构造器参数装饰器优先级最低。
元数据 对象、类等都是数据,它们描述了某种数据,而描述这些数据的数据就是元数据
reflect-metadata 库通过添加对元数据的支持,使得装饰器能够更方便地访问和修改类和方法的元数据,也许还会提示安装tslib
安装:npm i reflect-metadata tslib
引入,扩展了全局API Reflect 1 import 'reflect-metadata'
在装饰器中可以拿到类、方法、访问符、属性、参数的基本信息,如名称,描述符等。获取更多信息就需要元数据。
在编译过程中产生的元数据是非常重要的信息,在 nestjs 框架中 DI 和 IOC 的实现就依赖了他们。
参考:深入浅出Typescript装饰器 TS 装饰器(2): 元数据
定义/获取元数据 Reflect.defineMetadata()
定义元数据:给对象、类等数据附加额外的描述信息,但又不会影响到数据本身。Reflect.getMetadata()
获取元数据。
1 2 3 4 5 6 7 8 Reflect .defineMetadata (key, value, classObject)Reflect .defineMetadata (key, value, classPrototype, methodName)Reflect .defineMetadata (key, value, classPrototype, propKey)Reflect .getMetadata (metadataKey, target, propertyKey?)
前两个参数分别是元数据的key和value,都是any类型 ,后面的参数设定了要将元数据定义给谁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const obj = { name : 'chuckle' , fn ( ){ console .log (this .name ); } } Reflect .defineMetadata ('testKey1' , 'testValue1' , obj)Reflect .defineMetadata ('testKey2' , 'testValue2' , obj, 'name' )Reflect .defineMetadata ('testKey3' , 'testValue3' , obj, 'fn' )console .log (Reflect .getMetadata ('testKey1' , obj)) console .log (Reflect .getMetadata ('testKey2' , obj, 'name' )) console .log (Reflect .getMetadata ('testKey3' , obj, 'fn' )) console .log (Reflect .getMetadata ('test' , obj))
元数据可以定义在不存在的对象成员上,并且能正常获取
1 2 3 const obj = {}Reflect .defineMetadata ('testKey' , 'testValue' , obj, 'name' )console .log (Reflect .getMetadata ('testKey' , obj, 'name' ))
在类中定义 Reflect.defineMetadata
多给普通对象定义元数据
类使用 @Reflect.metadata
元数据装饰器,更方便地定义元数据,参数只有key和value
类元数据直接定义在类的构造函数上,而类成员的元数据定义在原型对象对应的属性上(无论实际上是否存在该属性),实例也会被赋予类成员元数据的副本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Reflect .metadata ('testKey1' , 'testValue1' )class CLS { @Reflect .metadata ('testKey2' , 'testValue2' ) name = 'chuckle' @Reflect .metadata ('testKey3' , 'testValue3' ) fn ( ) { console .log (this .name ); } } const cls = new CLS ();console .log (Reflect .getMetadata ('testKey1' , CLS )) console .log (Reflect .getMetadata ('testKey2' , CLS .prototype , 'name' )) console .log (Reflect .getMetadata ('testKey3' , CLS .prototype , 'fn' )) console .log (Reflect .getMetadata ('testKey2' , cls, 'name' ))
其它方法 Reflect 还有一些方法
hasMetadata
判断指定目标是否存在指定key的元数据
hasOwnMetadata
判断指定目标是否存在指定key的元数据
getMetadataKeys
获取指定目标的所有的元数据key组成的数组,包括其父类的元数据key
getOwnMetadataKeys
获取指定目标自身的所有的元数据key组成的数组,不包括其父类的元数据key
1 2 3 4 5 6 7 8 9 10 @Reflect .metadata ('key1' , 'value1' )class Parent { }@Reflect .metadata ('key2' , 'value2' )class Child extends Parent { }console .log (Reflect .getMetadataKeys (Child )) console .log (Reflect .getOwnMetadataKeys (Child )) console .log (Reflect .hasMetadata ('key1' , Child )) console .log (Reflect .hasOwnMetadata ('key1' , Child ))
内置元数据 开启emitDecoratorMetadata
配置,TS 会在编译后自动给类和类成员添加如下元数据:
design:type
:被装饰目标的类型
成员属性:属性的标注类型
成员方法:Function 类型
design:paramtypes
: 被装饰目标的参数类型
成员方法:方法形参列表的标注类型
类:构造函数形参列表的标注类型
design:returntype
tsconfig.json 1 2 "emitDecoratorMetadata" : true ,
注意:
只有你已经给目标添加了元数据,TS才会自动为这些目标添加内置元数据。
标注类型,即在代码中显式标注参数、返回值、属性等的类型,而不能靠自动推断。
通过 design:paramtypes
可以获取到方法中有多少个参数,每个参数的类型等信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Reflect .metadata ('key' , 'value' )class People { @Reflect .metadata ('key' , 'value' ) name : string constructor (name: string , age: number ) { this .name = name } @Reflect .metadata ('key' , 'value' ) setName (name : string ): string { this .name = name return this .name } } console .log (Reflect .getMetadata ('design:paramtypes' , People ))console .log (Reflect .getMetadata ('design:paramtypes' , People .prototype , 'setName' ))console .log (Reflect .getMetadata ('design:returntype' , People .prototype , 'setName' ))console .log (Reflect .getMetadata ('design:type' , People .prototype , 'name' ))
总之就是将你在代码中显式标注的类型信息,转为对应元数据定义在对应的类或类成员上
元数据的继承 元数据可以很好的适配于类之间的继承关系,父类会将其元数据的副本赋予子类,包括类本身和类成员的元数据
1 2 3 4 5 6 7 class Parent { @Reflect .metadata ('aa' , 'aa' ) aa = 1 } class Child extends Parent { }console .log (Reflect .getMetadataKeys (Child .prototype , 'aa' ))
搭建TS环境 使用Rollup、Webpack等打包构建工具开发TS项目
Rollup 详见:Rollup#配置TS环境
我的tsconfig.json 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 { "compilerOptions" : { "incremental" : true , "tsBuildInfoFile" : "./buildFile" , "diagnostics" : true , "target" : "esnext" , "module" : "esnext" , "lib" : [ "esnext" , "dom" , "dom.iterable" , ] , "allowJs" : true , "checkJs" : true , "outDir" : "./dist" , "rootDir" : "./src" , "declaration" : true , "declarationDir" : "./dist/typings" , "sourceMap" : false , "declarationMap" : false , "types" : [ ] , "removeComments" : true , "noEmit" : true , "noEmitOnError" : true , "noEmitHelpers" : true , "importHelpers" : true , "downlevelIteration" : true , "strict" : true , "alwaysStrict" : true , "noImplicitAny" : true , "strictNullChecks" : true , "strictFunctionTypes" : true , "strictPropertyInitialization" : true , "strictBindCallApply" : true , "noImplicitThis" : true , "noUnusedLocals" : true , "noUnusedParameters" : true , "noFallthroughCasesInSwitch" : true , "noImplicitReturns" : true , "esModuleInterop" : true , "allowUmdGlobalAccess" : true , "moduleResolution" : "node" , "baseUrl" : "./" , "paths" : { "@/*" : [ "src/*" ] } , "rootDirs" : [ "src" ] , "listEmittedFiles" : true , "listFiles" : true , "experimentalDecorators" : true , "emitDecoratorMetadata" : true , "resolveJsonModule" : true } , "include" : [ "src/**/*" , ] , }
Webpack 详见:Webpack#配置TS环境
esbuild+swc 安装:npm i @swc/core esbuild @swc/helpers -D
@swc/core 是 swc 的核心包,用于编译 JavaScript 和 TypeScript 代码;esbuild 是一个快速的 JavaScript 和 TypeScript 构建工具;@swc/helpers 是 swc 的辅助包,用于转换 JSX 代码
运行:node ./config.mjs
config.mjs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import esbuild from 'esbuild' import swc from '@swc/core' import fs from 'node:fs' esbuild.build ({ entryPoints : ['./src/index.ts' ], bundle : true , loader : { '.js' : 'js' , '.ts' : 'ts' , '.jsx' : 'jsx' , '.tsx' : 'tsx' , }, treeShaking :true , define : { 'process.env.NODE_ENV' : '"production"' , }, plugins : [ { name : "swc-loader" , setup (build ) { build.onLoad ({ filter : /\.(js|ts|tsx|jsx)$/ }, (args ) => { const content = fs.readFileSync (args.path , "utf-8" ) const { code } = swc.transformSync (content, { filename : args.path }) return { contents : code } }) }, } ], outdir : "dist" })
实战 写一些小玩意练练手
封装LocalStorage 实验 Rollup + TS 封装一个支持过期时间的LocalStorage
src/enum/index.ts 1 2 3 4 5 6 7 export enum Dictionaries { expire = '__expire__' , permanent = 'permanent' }
src/types/index.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import { Dictionaries } from "../enum" ;export type Key = string export type expire = Dictionaries .permanent | number export interface Data <T> { value : T, [Dictionaries .expire ]: expire } export interface Result <T> { message : string , value : T | null } export interface StorageCls { set : <T>(key: Key, value: T, expire: expire ) => void get : <T>(key: Key ) => Result <T | null > remove : (key: Key ) => void clear : () => void }
src/index.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 import { Dictionaries } from "./enum" ;import { Key , Result , StorageCls , expire, Data } from "./types" ;class LLS implements StorageCls { set<T = any >(key : Key , value : T, expire : expire): void { const data : Data <T> = { value, [Dictionaries .expire ]: expire } localStorage .setItem (key, JSON .stringify (data)) } get<T = any >(key : Key ): Result <T | null > { const data : Data <T> | null = JSON .parse (localStorage .getItem (key) || 'null' ) const result : Result <T | null > = { message : "" , value : null } if (data === null ) { result.message = `找不到${key} ` } else if (this .isOverdue (data[Dictionaries .expire ])) { result.message = `${key} 已过期` this .remove (key) } else { result.message = `获取${key} 成功` result.value = data.value } return result } remove (key : string ): void { localStorage .removeItem (key) } clear (): void { localStorage .clear () } private isOverdue (expire : expire): boolean { const now = new Date ().getTime () if (expire === Dictionaries .permanent ) { return false } else if (typeof expire === 'number' ) { return expire < now ? true : false } return false } } const sl = new LLS ()sl.set <number >('a' , 123 , new Date ().getTime () + 5000 ) setInterval (() => { const a = sl.get <number >('a' ) console .log (a) }, 500 )
发布订阅模式 实现个类似addEventListener、Vue evnetBus的发布订阅模式Demo
一个消息可以挂载多个订阅方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 interface EventFun { (...args : any []): any } interface EventCls { on (name : string , callback : EventFun ): void emit (name : string , ...args : any []): void off (name : string , fn : EventFun ): void once (name : string , fn : EventFun ): void } type CallbackArr = Array <EventFun >interface EventList { [key : string ]: CallbackArr , } class SubPub implements EventCls { list : EventList constructor ( ) { this .list = {} } on (name: string , callback: EventFun ) { const callbackList : CallbackArr = this .list [name] || []; callbackList.push (callback) this .list [name] = callbackList } emit (name: string , ...args: any [] ) { const callbackList : CallbackArr = this .list [name] if (callbackList) { if (callbackList.length <= 0 ) { console .warn ("该消息没有订阅者" ) return ; } callbackList.forEach (callback => { callback.apply (this , args) }) } else { console .warn ("没有该消息" ) } } off (name: string , fn: EventFun ) { const callbackList : CallbackArr = this .list [name] if (callbackList) { if (callbackList.length <= 0 ) { console .warn ("该消息没有订阅者" ) return ; } const index = callbackList.findIndex (fns => fns === fn) index > -1 ? callbackList.splice (index, 1 ) : null } else { console .warn ("没有该消息" ) } } once (name: string , fn: EventFun ) { const decor : EventFun = (...args ) => { fn.apply (this , args) this .off (name, decor) } this .on (name, decor) } }
测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const subPub = new SubPub ()subPub.emit ('abc' , 678 ) const fn : EventFun = (...arg ) => { console .log (arg); } subPub.on ('abc' , fn) subPub.emit ('abc' , 131 , true ) subPub.emit ('abc' , 678 , false , 'qx' ) subPub.off ('abc' , fn) subPub.emit ('abc' , 321 , 'qx' ) console .log ("=======================" );subPub.emit ('a' , 678 ) subPub.once ('a' , (...arg ) => { console .log (arg); }) subPub.emit ('a' , 678 , 'abc' ) subPub.emit ('a' , 123 , 'qx' ) subPub.on ('a' , (...arg ) => { console .log (arg); }) subPub.emit ('a' , 123 , 'qx' )
Proxy、Reflect Proxy(代理)和Reflect(反射)是ES6为了操作对象而提供的新API,它们为开发者提供了对对象行为进行拦截和自定义的能力
Proxy和Reflect都有13个名字和参数一模一样的方法,因为对象的操作无非就get、set等
ES6推荐用Proxy去代理对对象的操作,用Reflect实现对对象的实际操作
基本操作 Proxy基本操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 interface ProxyHandler <T extends object > { apply?(target : T, thisArg : any , argArray : any []): any ; construct?(target : T, argArray : any [], newTarget : Function ): object ; defineProperty?(target : T, property : string | symbol, attributes : PropertyDescriptor ): boolean ; deleteProperty?(target : T, p : string | symbol): boolean ; get?(target : T, p : string | symbol, receiver : any ): any ; getOwnPropertyDescriptor?(target : T, p : string | symbol): PropertyDescriptor | undefined ; getPrototypeOf?(target : T): object | null ; has?(target : T, p : string | symbol): boolean ; isExtensible?(target : T): boolean ; ownKeys?(target : T): ArrayLike <string | symbol>; preventExtensions?(target : T): boolean ; set?(target : T, p : string | symbol, newValue : any , receiver : any ): boolean ; setPrototypeOf?(target : T, v : object | null ): boolean ; }
Reflect基本操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 declare namespace Reflect { function apply<T, A extends readonly any [], R>( target : (this : T, ...args: A ) => R, thisArgument : T, argumentsList : Readonly <A>, ): R; function apply (target: Function , thisArgument: any , argumentsList: ArrayLike<any > ): any ; function construct<A extends readonly any [], R>( target : new (...args : A) => R, argumentsList : Readonly <A>, newTarget?: new (...args : any ) => any , ): R; function construct (target: Function , argumentsList: ArrayLike<any >, newTarget?: Function ): any ; function defineProperty (target: object , propertyKey: PropertyKey, attributes: PropertyDescriptor & ThisType<any > ): boolean ; function deleteProperty (target: object , propertyKey: PropertyKey ): boolean ; function get<T extends object , P extends PropertyKey >( target : T, propertyKey : P, receiver?: unknown, ): P extends keyof T ? T[P] : any ; function getOwnPropertyDescriptor<T extends object , P extends PropertyKey >( target : T, propertyKey : P, ): TypedPropertyDescriptor <P extends keyof T ? T[P] : any > | undefined ; function getPrototypeOf (target: object ): object | null ; function has (target: object , propertyKey: PropertyKey ): boolean ; function isExtensible (target: object ): boolean ; function ownKeys (target: object ): (string | symbol)[]; function preventExtensions (target: object ): boolean ; function set<T extends object , P extends PropertyKey >( target : T, propertyKey : P, value : P extends keyof T ? T[P] : any , receiver?: any , ): boolean ; function set (target: object , propertyKey: PropertyKey, value: any , receiver?: any ): boolean ; function setPrototypeOf (target: object , proto: object | null ): boolean ; }
案例 判断成年的案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 let person = { name : 'chuckle' , age : 20 , } let personProxy = new Proxy (person, { get (target, p, receiver ) { if (target.age < 18 ) { return Reflect .get (target, p, receiver) } else { return '已成年' } } }) console .log (personProxy.age ) Reflect .set (person, 'age' , 16 )console .log (personProxy.age )
实现简单的响应式Demo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const ref = <T extends object >(params : T): T => { return new Proxy (params, { get (target, key, receiver): any { return Reflect .get (target, key, receiver) }, set (target, key, value, receiver): boolean { const oldValue = Reflect .get (target, key, receiver); console .log (`set [${String (key)} ] from "${oldValue} " to "${value} "` ); const result = Reflect .set (target, key, value, receiver); return result; } }) }; const person = ref ({ name : 'chuckle' , age : 20 , }); console .log (person); person.age = 16 ;
类型兼容性 协变(鸭子类型) 协变:鸭子类型 在程序设计中是动态类型的一种风格。 在这种风格中,一个物件有效的语义,不是由继承自特定的类或实现特定的接口决定,而是由当前方法和属性的集合 决定
一只鸟走路像鸭子,游泳也像,做什么都像,那么这只鸟就可以成为鸭子类型。
当子类型中的属性满足父类型就可以进行赋值,即协变不能少可以多
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 interface Bird { name : string ; } interface Duck { name : string ; say (): void } let bird : Bird = { name : '鸟' , } let duck : Duck = { name : '鸭子' , say ( ) { console .log (this .name ); } } bird = duck
Duck类型包含了Bird类型,且比Bird类型更具体,属性更多,从继承角度讲,Duck就是Bird的子类型,而子类型当然可以赋值给父类型,但会抛弃子类型中不属于父类型的多余属性
逆变 逆变同样是子类型赋给父类型,多发生在函数参数上
接收子类型的函数变量不能赋给接收父类型的函数变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 interface Bird { name : string ; } interface Duck { name : string ; say (): void } let fnBird = (params: Bird ) => { console .log (params.name ); } let fnDuck = (params: Duck ) => { console .log (params.name ); params.say (); } fnBird = fnDuck fnDuck = fnBird
函数变量发生赋值后,调用fnBird()
,实际上仍然调用的是(params: Duck)=>{}
,若是传入params: Bird
,其成员不足以覆盖params: Duck
,params.say()
会报错,所以是不安全的,仍然要符合不能少可以多
双向协变 这是一种不安全的类型兼容操作,在TS2.0后需要关闭严格的函数类型检查,来允许函数参数双向协变
1 2 3 "compilerOptions" : { "strictFunctionTypes" : false , }
现在函数变量可以自由赋值
1 2 3 4 5 6 7 8 9 let fnBird = (params: Bird ) => { console .log (params.name ); } let fnDuck = (params: Duck ) => { console .log (params.name ); params.say (); } fnBird = fnDuck fnDuck = fnBird
因为保存函数的仅仅是一个变量,赋值后到另一个函数变量后调用,再调用的仍然同一个函数,看起来没问题,但若是在赋值后再调用原本的变量调用函数,就会存在问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 interface Bird { name : string ; } interface Duck { name : string ; say (): void } let bird : Bird = { name : '鸟' , } let duck : Duck = { name : '鸭子' , say ( ) { console .log (this .name ); } } let fnBird = (params: Bird ) => { console .log (params.name ); } let fnDuck = (params: Duck ) => { console .log (params.name ); params.say (); } fnBird = fnDuck fnBird (bird) fnDuck (duck)
fnBird在经过赋值后,实际的函数是fnDuck,需要的是Duck类型参数,但仍然传入的是Bird类型参数,父类型赋给子类型当然会出错
所以,不要使用双向协变 ,这可能导致运行时的意外错误
Set,Map set和map是ES6新增的引用数据类型,详见:ES6查缺补漏
Set类似于数组,但自动去重
1 2 3 4 5 6 7 const arr : number [] = [1 , 2 , 3 , 3 , 2 , 1 ];const set : Set <number > = new Set (arr);set.add (4 ); console .log (set); set.delete (4 ); console .log (set); console .log (set.has (4 ));
Map类似对象,但键可以是任意类型
1 2 3 4 5 6 7 8 9 10 const arr : [any , any ][] = [ ['x' , 1 ], ['y' , 2 ], ] const map : Map <any , any > = new Map (arr);console .log (map); map.set ('z' , 3 ); console .log (map.get ('z' )); console .log (map.keys ()); console .log (map.values ());
weakSet,weakMap Weak在英语的意思是弱 ,表示weakSet 的值、weakMap 的键必须是引用类型,且引用是弱引用,不计入垃圾回收策略,如果没有其他的对Weak中对象的引用存在,那么这些对象会被垃圾回收
垃圾回收: JavaScript引擎在值“可达”和可能被使用时会将其保持在内存中,否则会自动进行垃圾回收
1 2 3 4 5 let john = { name : "John" };john = null ;
通常,当对象、数组之类的数据结构在内存中时,它们的子元素,如对象的属性、数组的元素都被认为是可达的。
例如,如果把一个对象放入到Map中作为键,那么只要这个Map存在,那么这个对象也就存在,即使没有其他对该对象的引用。
1 2 3 4 5 6 let map = new Map ();map.set (john, "..." ); john = null ; console .log (map.keys ())
WeakMap在这方面有着根本上的不同。它不会阻止垃圾回收机制对作为键的对象(key object)的回收
1 2 3 4 5 6 7 8 9 10 11 let john : any = { name : "John" };let weakMap = new WeakMap ();weakMap.set (john, "..." ); john = null ;
WeakMap和WeakSet在第三方库处理外部数据、或用作缓存时非常有用,可以防止垃圾数据堆积在内存中
1 2 3 4 5 6 7 8 9 10 11 let visitsCountMap = new WeakMap (); function countUser (user: object ) { let count = visitsCountMap.get (user) || 0 ; visitsCountMap.set (user, count + 1 ); } let john : any = { name : "John" };countUser (john); john = null ;
内置高级类型 TS内置了许多高级类型(常用的工具类型),可以看作是类型的函数,接收一个类型,返回加工后的类型
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 type Partial <T> = { [P in keyof T]?: T[P]; }; type Required <T> = { [P in keyof T]-?: T[P]; }; type Readonly <T> = { readonly [P in keyof T]: T[P]; }; type Pick <T, K extends keyof T> = { [P in K]: T[P]; }; type Record <K extends keyof any , T> = { [P in K]: T; }; type Exclude <T, U> = T extends U ? never : T;type Omit <T, K extends keyof any > = Pick <T, Exclude <keyof T, K>>;
Partial Partial<T>
可以将原来的类型变为可选 的
1 2 3 4 5 6 7 8 9 10 11 interface Person { name : string age : number , gender : 'male' | 'female' } type p1 = Partial <Person >
keyof 获取一个对象类型的所有键,作为联合类型,in 是映射类型语法,用于遍历keyof T 让联合类型的每一项作为属性,? 将每一个属性变成可选项,T[P] 索引访问操作符,与访问属性值的操作类似
1 2 3 type Partial <T> = { [P in keyof T]?: T[P]; };
Pick 筛选类型的属性 。从类型的属性中,选取指定一组属性,返回一个新的类型定义。
1 2 3 4 5 6 7 8 9 10 interface Person { name : string age : number , gender : 'male' | 'female' } type p2 = Pick <Person , 'name' | 'age' >
K extends keyof T 限制key必须是传入类型的属性,in 遍历K 让联合类型的每一项作为属性,T[P] 索引访问操作符
1 2 3 type Pick <T, K extends keyof T> = { [P in K]: T[P]; };
Readonly 将类型中所有属性变为只读
1 2 3 4 5 6 7 8 9 10 11 interface Person { name : string age : number , gender : 'male' | 'female' } type p3 = Readonly <Person >
源码上和Partial很像,只是把 ? 换为了readonly
1 2 3 4 5 6 type Readonly <T> = { readonly [P in keyof T]: T[P]; };
Record 构造一个具有一组属性 K,属性类型为 T 的类型,做到同时对 key 和 value 进行类型定义
1 2 3 4 5 6 7 8 9 interface Person { name : string age : number , gender : 'male' | 'female' } type persons = Record <string , Person >
keyof any
: any 可以代表任何类型。那么任何类型的 key 都可能为 string 、 number 或者 symbol 。所以自然 keyof any 为 string | number | symbol 的联合类型。K extends keyof any
: 类型约束,表示类型参数 K 必须是类型 string | number | symbol 的子集或相同类型。[P in K]
表示对类型 K 中的每个属性键 P 进行映射(遍历)。其中 K 是一个联合类型或是一个具有多个属性的类型
1 2 3 4 5 type Record <K extends keyof any , T> = { [P in K]: T; }; type keys = keyof any
Omit 去掉某些属性
1 2 3 4 5 6 7 8 9 10 interface Person { name : string age : number , gender : 'male' | 'female' } type p4 = Omit <Person , 'gender' >
1 2 3 4 type Exclude <T, U> = T extends U ? never : T;type Omit <T, K extends keyof any > = Pick <T, Exclude <keyof T, K>>;
ReturnType ReturnType<T>
获取函数返回值的类型
1 2 3 type ReturnType <T extends (...args : any ) => any > = T extends (...args : any ) => infer R ? R : any ;const fn = ( ) => 'hello' type T = ReturnType <typeof fn>;
类型工具 内置高级类型就是类型工具,但还有一些常用的类型工具并没有内置,需要自己实现
1、将部分属性变为可选
1 2 3 4 5 6 7 8 9 10 11 12 interface Person { name : string age : number , gender : 'male' | 'female' } type Optional <T, K extends keyof T> = Omit <T, K> & Partial <Pick <T, K>>type newPerson = Optional <Person , 'gender' >
占位符infer infer 关键字定义一个占位符,可以自动推断类型,简化代码
案例:传入一个类型,若是数组类型,则返回数组元素的类型组成的联合类型,否则原样返回
1 2 3 4 5 6 type TypeDeconstruct <T> = T extends Array <any > ? T[number ] : Tconst arr = [1 , 'a' , 2 , 'b' ];type A = TypeDeconstruct <typeof arr> type B = TypeDeconstruct <boolean >
使用infer 自动推断数组中元素的类型
1 2 3 4 5 type TypeDeconstruct <T> = T extends Array <infer U> ? U : Tconst arr = [1 , 'a' , 2 , 'b' ];type A = TypeDeconstruct <typeof arr> type B = TypeDeconstruct <boolean >
提取/删除元素 1、提取头部元素
1 2 3 type Arr = ['a' ,'b' ,'c' ]type First <T extends any []> = T extends [infer First ,...any []] ? First : []type a = First <Arr >
2、提取尾部元素
1 2 3 type Arr = ['a' , 'b' , 'c' ]type Last <T extends any []> = T extends [...any [], infer Last ,] ? Last : []type a = Last <Arr >
3、剔除第一个元素 Shift
1 2 3 type Arr = ['a' ,'b' ,'c' ]type Shift <T extends any []> = T extends [unknown,...infer Rest ] ? Rest : []type a = Shift <Arr >
4、剔除尾部元素 Pop
1 2 3 type Arr = ['a' ,'b' ,'c' ]type Pop <T extends any []> = T extends [...infer Rest ,unknown] ? Rest : []type a = Pop <Arr >
递归 通过递归将类型数组逆序:
1 2 3 type Arr = ['a' ,'b' ,'c' ]type ReveArr <T extends any []> = T extends [infer First , ...infer rest] ? [...ReveArr <rest>, First ] : Ttype Res = ReveArr <Arr >
提取指定位置上的元素:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 type Arr = ['a' , 'b' , 'c' ];type IndexOf <T extends any [], K extends number , Acc extends any [] = []> = { 0 : Acc [0 ]; 1 : K extends 0 ? Acc [0 ] : IndexOf <Tail <T>, K, [Head <T>, ...Acc ]>; }[K extends Acc ['length' ] ? 0 : 1 ]; type Head <T extends any []> = T extends [infer H, ...any []] ? H : never ;type Tail <T extends any []> = T extends [infer _H, ...infer R] ? R : [];type a = IndexOf <Arr , 1 >; type b = IndexOf <Arr , 2 >; type c = IndexOf <Arr , 3 >;