TypeScript是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。

| TypeScript | JavaScript |
|---|---|
| JavaScript 的超集,用于解决大型项目的代码复杂性 | 一种脚本语言,用于创建动态网页。 |
| 强类型,支持静态和动态类型 | 动态弱类型语言 |
| 可以在编译期间发现并纠正错误 | 只能在运行时发现错误 |
| 不允许改变变量的数据类型 | 变量可以被赋予不同类型的值 |
npm install -g typescript // yarn方式 yarn global add typescript// 检查是否安装成功tsc -v// 编译ts文件tsc index.tsvscode中监听ts文件自动编译
// 1. 初始化tsconfig.json文件tsc --init 2. 左上角菜单点击终端->运行任务->点击typescript->选择tsc监视tsconfig文件let isDone: boolean = false;// let isDone: boolean = "123"; //Type 'string' is not assignable to type 'boolean'赋值与定义的不一致,会报错,静态类型语言的优势就体现出来了,可以帮助我们提前发现代码中的错误。
let decimal: number = 6;let color: string = "blue";let list: number[] = [1, 2, 3];// let list: Array<number> = [1, 2, 3];// 数组里的项写错类型会报错let list: number[] = [1, 2, "3"]; // error// push 时类型对不上会报错list.push(4)list.push("4") // error如果数组想每一项放入不同类型数据,怎么办?那我们可以使用元组类型
元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。
let x: [string, number] = ["hello", 10];// 写错类型会报错let x: [string, number] = [10, "hello"]; // Error// 越界会报错let x: [string, number] = ["hello", 10, 110]; // Error// 使用 push 时,不会有越界报错,但是只能 push 定义的 number 或者 string 类型x.push("hello2")x.push(11)console.log(x) //['hello', 10, 'hello2', 11]// push 一个没有定义的类型,报错x.push(true) // errorlet u:undefined = undefined let n:null = null默认情况下 null 和 undefined 是所有类型的子类型。就是说你可以把 null 和 undefined 赋值给 number 类型的变量。
let age: number = nulllet realName: string = undefined但是如果指定了 --strictNullChecks 标记,null 和 undefined 只能赋值给 void 和它们各自,不然会报错。
不清楚用什么类型,可以使用 any 类型
let notSure: any = 4notSure = "maybe a string" // 可以是 string 类型notSure = false // 也可以是 boolean 类型notSure.name // 可以随便调用属性和方法notSure.getName()不过,不建议使用 any,不然就丧失了 TS 的意义。
不建议使用 any,当我不知道一个类型具体是什么时,该怎么办?
可以使用 unknown 类型
把 param 定义为 any 类型,TS 就能编译通过,没有把潜在的风险暴露出来,万一传的不是 number 类型,不就没有达到预期了吗。
function divide(param: any) { return param / 2;}把 param 定义为 unknown 类型 ,TS 编译器就能拦住潜在风险
因为不知道 param 的类型,使用运算符 /,导致报错。
function divide(param: unknown) { return param / 2; // error}配合类型断言,可以解决这个问题
function divide(param: unknown) { return param as number / 2;}void类型与 any 类型相反,它表示没有任何类型。
比如函数没有明确返回值,默认返回 Void 类型
function welcome(): void { console.log('hello')}never类型表示的是那些永不存在的值的类型。
有些情况下值会永不存在,比如,
// 异常function fn(msg: string): never { throw new Error(msg)}// 死循环function fn(): never { while (true) {}}定义一个数字枚举
enum Color { Red, Green, Blue,}console.log(Color.Red) // 0console.log(Color.Green) // 1console.log(Color.Blue) // 2console.log(Color[0]) // Red特点:
如果枚举第一个元素赋有初始值,就会从初始值开始递增
enum Color { Red=3, Green, Blue,}console.log(Color.Red) // 3console.log(Color.Green) // 4console.log(Color.Blue) // 5定义一个枚举来管理外卖状态,分别有已下单,配送中,已接收三个状态。
enum Status { Buy = 1, Send, Receive}console.log(Status.Buy) // 1console.log(Status.Send) // 2console.log(Status.Receive) // 3但有时候后端给你返回的数据状态是乱的,就需要我们手动赋值。
比如后端说 Buy 是 100,Send 是 20,Receive 是 1,就可以这么写:
enum Status { Buy = 100, Send = 20, Receive = 1}console.log(Status.Buy) // 100console.log(Status.Send) // 20console.log(Status.Receive) // 1枚举中的成员可以被计算
enum FileAccess { // constant members None, Read = 1 << 1, Write = 1 << 2, ReadWrite = Read | Write, // computed member G = "123".length}console.log(FileAccess.None) // 0console.log(FileAccess.Read) // 2 -> 010console.log(FileAccess.Write) // 4 -> 100console.log(FileAccess.ReadWrite) // 6 -> 110console.log(FileAccess.G) // 3字符串枚举意义在于,提供有具体语义的字符串,可以更容易地理解代码和调试。
enum Direction { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT",}const value = 'UP'if (value === Direction.Up) { // do something}常量枚举编译出来的 JS 代码会简洁很多,提高了性能。常量枚举不允许包含计算成员
const enum Direction { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT",}const value = 'UP'if (value === Direction.Up) { // do something}会被编译成:
const value = 'UP';if (value === "UP" /* Up */) { // do something}不写const会被编译成:
(function (Direction) { Direction["Up"] = "UP"; Direction["Down"] = "DOWN"; Direction["Left"] = "LEFT"; Direction["Right"] = "RIGHT";})(Direction || (Direction = {}));const value = 'UP';if (value === Direction.Up) { // do something}这一堆定义枚举的逻辑会在编译阶段会被删除,常量枚举成员在使用的地方被内联进去。
当枚举作为类型时,表示该属性只能为枚举中的某一个成员
enum SEX{ man = '男', woman = '女', unknown = '未知'}let arr:Array<SEX> = [SEX.man,SEX.woman]let s:SEX = SEX.manconsole.log(s); // 男在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)。
在ts中,接口除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。
接口一般首字母大写
定义对象类型,可以对对象形状进行描述,接口和对象形状必须保持一致
interface IntfPerson { name: string; age: number;}const jack: IntfPerson = { name: 'jack', age: 25}// 缺少或多出属性都会报错const jack: IntfPerson = { name: 'jack'}const jack: IntfPerson = { name: 'jack', age: 25, city: '深圳'}如果不需要完全匹配形状,可以使用可选属性
interface IntfPerson { name: string; age?: number;}const jack: IntfPerson = { name: 'jack'}有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用 readonly 定义只读属性
interface IntfPerson { readonly id: number; name: string; age?: number;}let jack: IntfPerson = { id: 89757, name: 'Tom',};// 报错jack.id = 9527;interface 也可以用来描述函数类型
interface ISum { (x:number,y:number):number}const add:ISum = (num1, num2) => { return num1 + num2}console.log(add(1,2)); // 3先了解下es6中类的用法:
class Animal { public name; constructor(name:string) { this.name = name; } sayHi() { return `My name is ${this.name}`; }}let a = new Animal('Kerry');console.log(a.sayHi()); // My name is Kerryclass Cat extends Animal { constructor(name:string) { super(name); // 调用父类的 constructor(name) console.log(this.name); } sayHi() { return 'Meow, ' + super.sayHi(); // 调用父类的 sayHi() }}let c = new Cat('Tom'); // Tomconsole.log(c.sayHi()); // Meow, My name is Tomclass Animal { constructor(name:string) { this.name = name; } get name() { return 'Kerry'; } set name(value) { console.log('setter: ' + value); }}let a = new Animal('Kitty'); // setter: Kittya.name = 'Tom'; // setter: Tomconsole.log(a.name); // Kerryclass Animal { static cry() { console.log('会哭'); // 会哭 }}Animal.cry(); // 报错const animal = new Animal()animal.cry() // errorclass Animal { name = 'Kerry'; constructor() { // ... }}let a = new Animal();console.log(a.name); // Kerryclass Animal { static num = 42; constructor() { // ... }}console.log(Animal.num); // 42ts里面定义属性的时候,提供了三种修饰符
public:公有,在类里面、子类、类外面都可以访问
protected:保护类型,在类里面、子类里面可以访问,在类外面没法访问
private:私有,在类里面可以访问,子类、类外部都没法访问
属性如果不加修饰符,默认就是公有public
class Person { public name:string // protected name:string // private name:string constructor(name:string){ this.name = name } run():string{ return `${this.name}在运动` }}class Web extends Person { constructor(name:string){ super(name) // 初始化父类的构造函数 } run():string{ return `${this.name}在运动-子类` } work(): string { return `${this.name}在工作` }}const w = new Web('kerry')console.log(w.run()); // kerry在运动-子类console.log(w.work()); // kerry在工作console.log(w.name); // kerryabstract class Animal { public name; public constructor(name:string) { this.name = name; }}let a = new Animal('Kerry'); // errorabstract class Animal { public name; public constructor(name:string) { this.name = name; } public abstract sayHi():void;}class Cat extends Animal { public sayHi() { console.log(`Meow, My name is ${this.name}`); }}let cat = new Cat('Kerry');class Animal { name: string; constructor(name: string) { this.name = name; } sayHi(): string { return `My name is ${this.name}`; }}let a: Animal = new Animal('Kerry');console.log(a.sayHi()); // My name is Kerry一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),
用 implements 关键字来实现,大大提高了面向对象的灵活性
这里举个例子:
门是一个类,防盗门是门的子类。如果防盗门有一个报警器的功能,我们可以简单的给防盗门添加一个报警方法。这时候如果有另一个类,车,也有报警器的功能,就可以考虑把报警器提取出来,作为一个接口,防盗门和车都去实现它
interface Alarm { alert(): void;}class Door {}class SecurityDoor extends Door implements Alarm { alert() { console.log('SecurityDoor alert'); }}class Car implements Alarm { alert() { console.log('Car alert'); }}interface Alarm { alert(): void;}interface Light { lightOn(): void; lightOff(): void;}class Car implements Alarm, Light { alert() { console.log('Car alert'); } lightOn() { console.log('Car light on'); } lightOff() { console.log('Car light off'); }}interface Alarm { alert(): void;}interface LightableAlarm extends Alarm { lightOn(): void; lightOff(): void;}class Car implements LightableAlarm { lightOn(): void { console.log('Car light on'); } lightOff(): void { console.log('Car light off'); } alert(): void { console.log('Car light on'); }}语法:值 as 类型
使用断言,目的是告诉ts,我比你更清楚这个参数是什么类型,以此防止报错
function getLength(arg: number | string): number { const str = arg as string if (str.length) { return str.length } else { const number = arg as number return number.toString().length }}console.log(getLength(123)); // 3console.log(getLength("123")); // 3// 需要注意的是,如果把一个类型断言成联合类型中不存在的类型则会报错function getLength(arg: number | string): number { return (arg as number[]).length // error}ts里,在有些没有明确指出类型的地方,类型推论会帮助提供类型。
let aa = 18a = 'hello'// 因为赋值的时候赋的是一个字符串类型,所以ts自动推导出userName是一个string类型。let userName = 'lin'// userName = 123 //再修改时会报错// 会自动推导为number类型function printAge(num = 18) { console.log(num) return num}printAge(20)// printAge("Kerry") // error// 定义没有返回值的函数,会自动推导返回值为void类型function welcome() { console.log('hello')}welcome()如果希望一个变量可以支持多种类型,就可以用联合类型(union types)来定义。
例如,一个变量既支持 number 类型,又支持 string 类型
// 联合类型大大提高了类型的可扩展性let num: number | string | booleannum = 8num = 'eight'num = true如果要对对象形状进行扩展,可以使用交叉类型 &
比如 Person 有 name 和 age 的属性,而 Student 在 name 和 age 的基础上还有 grade 属性
这和类的继承是一模一样的,这样 Student 就继承了 Person 上的属性,
interface Person { name: string age: number}type Student = Person & { grade: number }类型别名,就是给类型起个别名。类型别名用 type 关键字来书写,有了类型别名,我们书写 TS 的时候可以更加方便简洁。
比如这个例子,getName 这个函数接收的参数可能是字符串,可能是函数
type Name = stringtype NameResolver = () => stringtype NameOrResolver = Name | NameResolver // 联合类型function getName(n: NameOrResolver): Name { if (typeof n === 'string') { return n } else { return n() }}// 调用console.log(getName('kerry')); // kerryconsole.log(getName(() => 'kerry')); // kerry能用interface就用interface,实现不了再考虑用type
type IntfPerson = { name: string age: number}interface IntfPerson { name: string age: number}const person: IntfPerson = { name: 'kerry', age: 18}type addType = (num1:number,num2:number) => numberinterface addType { (num1:number,num2:number):number}const add:addType = (num1, num2) => { return num1 + num2}interface 使用 extends 实现继承, type 使用交叉类型实现继承
我们定义一个 IntfPerson 类型和 Student 类型,Student 继承自 IntfPerson
// interface 继承 interfaceinterface Person { name: string } interface Student extends Person { grade: number }// type 继承 typetype IntfPerson = { name: string }type Student = IntfPerson & { grade: number } // 用交叉类型// interface 继承 typetype IntfPerson = { name: string }interface Student extends IntfPerson { grade: number }// type 继承 interfaceinterface IntfPerson { name: string }type Student = IntfPerson & { grade: number } // 用交叉类型基本类型type Name = string// 联合类型type Name = string | number// 元组type Name = [string, number]交叉类型interface IntfPerson { name: string }type Student = IntfPerson & { grade: number }let myName:Name = 'kerry'let myName:Name = 123let myName:Name = ['kerry',2]let kerry:Student = { name:'kerry', grade: 1}interface User{ nage: string, age: number,}interface User{ sex: string}let user: User = { nage: '小明', age: 10, sex: '男'}泛型是指在定义函数、接口或类的时候,不预先指定具体类型,而是在使用的时候再指定类型。可以使得输入输出类型统一,且可以输入输出任何类型。
定义一个print函数,传入一个string类型参数并返回一个string类型
function print(arg:string):string { console.log(arg) return arg}假如这时候又多了一个number类型,则需要这样写
function print(arg:string | number):string | number { console.log(arg) return arg}const result:string | number = print(123)console.log('result',result); // 123function print<T>(arg:T):T { console.log(arg) return arg}// 两种方式使用:// 方式1:定义要使用的类型const result = print<number>(123)// 方式2:使用ts类型自动推断const result = print(123)interface IGeneric<T> { (arg: T): void} function fn<T>(arg: T): void { console.log(arg);} let myFn: IGeneric<number> = fn;myFn(13); //13定义一个栈,有入栈和出栈两个方法,如果想入栈和出栈的元素类型统一,写法如下:
class Stack<T> { private data: T[] = [] push(item:T) { return this.data.push(item) } pop():T | undefined { return this.data.pop() }}入栈和出栈是number类型const s1 = new Stack<number>()s1.push(1)console.log(s1.pop()); // 1假设现在有这么一个函数,打印传入参数的长度
function printLength<T>(arg: T): T { console.log(arg.length) // 因为不确定 T 是否有 length 属性,会报错 return arg}printLength([1,2,3])结合interface,通过extends来实现泛型约束
interface ILength { length: number}function printLength<T extends ILength>(arg: T): T { console.log(arg.length) return arg}printLength([1,2,3])function logClass(parmas:any){ parmas.prototype.apiUrl = "http://www.baidu.com"}@logClassclass HttpClient { constructor(){ } getData(){ }}const http:any = new HttpClient()console.log(http.apiUrl) // http://www.baidu.comfunction logClass(parmas:string){ return function(target:any){ console.log(parmas) console.log(target) }}@logClass('hello')class HttpClient { constructor(){ } getData(){ }}console打印出来结果:
helloindex.js:134 class HttpClient { constructor() { } getData() { }}接收2个参数,target当前类的原型对象,attr当前属性名称
function logClass(parmas:string){ return function(target:any){ console.log(parmas) console.log(target) }}function logProperty(params:string){ return function(target:any,attr:any){ console.log(target) // 当前类的原型对象 console.log(attr) // url // 修改属性值 target[attr] = params }}@logClass('hello')class HttpClient { @logProperty('http://api.xxx.com') public url: string | undefined constructor(){ } getData(){ console.log(this.url) // http://api.xxx.com }}const http = new HttpClient()http.getData()方法装饰器接收三个参数:1. target当前类原型的对象 2. 当前方法名 3. 当前方法的描述符
function get(params:string){ return function(target:any,methodName:any,desc:any){ console.log(target) // 当前类的原型对象 console.log(methodName) // 方法名 console.log(desc) // 描述符 // 和类装饰器一样,可以扩展当前类属性和方法 target.apiUrl='xxxx' target.run=function(){ console.log('run') } // 修改装饰器的方法 把装饰器方法里传入的所有参数改为string类型 // 1. 保存当前方法 2. 参数处理并调用oMethod方法 const oMethod = desc.value desc.value = function(...args:any[]){ args = args.map((value)=>{ return String(value) }) // 调用oMethod方法,并将参数args传入 // 与call不同,call接受参数列表,apply接受数组形式参数 oMethod.apply(this,args) } }}class HttpClient { public url: string | undefined constructor(){ } @get('http://api.xxx.com') getData(...args:any[]){ console.log(args); // ['123', '122'] console.log('我是getData里面的方法') }}const http:any = new HttpClient()console.log(http.apiUrl);http.run()http.getData(123,'123')参数装饰器会在运行时当做函数被调用,可以使用参数装饰器为类的原型增加一些元素数据,传入3个参数:
function logParams(params:any){ return function(target:any,methodName:any,paramsIndex:any){ console.log(params); // uuid console.log(target); // {constructor: ?, getData: ?} console.log(methodName); // getData console.log(paramsIndex); // 0 // 给类的原型增加属性 target.apiUrl = params }}class HttpClient { public url: string | undefined constructor(){ } getData(@logParams('uuid') uuid:any){ console.log(uuid); }}const http:any = new HttpClient()http.getData(123456)console.log(http.apiUrl); // uuidfunction logClass1(parmas:string){ return function(target:any){ console.log('类装饰器1') }}function logClass2(parmas:string){ return function(target:any){ console.log('类装饰器2') }}function logAttr(parmas?:string){ return function(target:any,attrName:any){ console.log('属性装饰器') }}function logMethod(parmas?:string){ return function(target:any,methodName:any,desc:any){ console.log('方法装饰器') }}function logParams1(parmas?:string){ return function(target:any,methodName:any,paramsIndex:any){ console.log('方法参数装饰器1') }}function logParams2(parmas?:string){ return function(target:any,methodName:any,paramsIndex:any){ console.log('方法参数装饰器2') }}@logClass1('hello')@logClass2('hello')class HttpClient { @logAttr() public apiUrl:string | undefined constructor(){ } @logMethod() getData(){ } setData(@logParams1() attr1:any,@logParams2() attr2:any){ }}const http = new HttpClient()执行以上代码,可以看到执行顺序是:
属性>方法>方法参数>类
如果有多个同样的装饰器,它会先执行后面的
当使用第三方库时,很多三方库不是用 TS 写的,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。
在ts中如果直接使用Vue,就会报错
const app = new Vue({ el: '#app', data: { message: 'Hello Vue!' }})这时,我们可以使用 declare 关键字来定义 Vue 的类型,这样就不会报错了
interface VueOption { el: string, data: any}declare class Vue { options: VueOption constructor(options: VueOption)}const app = new Vue({ el: '#app', data: { message: 'Hello Vue!' }})通常我们会把声明语句放到一个单独的文件(xxx.d.ts)中,这就是声明文件,以 .d.ts 为后缀。
一般来说,ts 会解析项目中所有的 *.ts 文件,当然也包含以 .d.ts 结尾的文件。
在使用第三方库的时候,如果社区已经为其提供了对应的类型文件,那么我们就不需要自己再声明一次了,直接安装即可
例如:
// 安装 lodash 的类型包npm install @types/lodash -DJavaScript中有很多内置对象,它们可以直接在 TypeScript 中当做定义好了的类型。
内置对象是指根据标准在全局作用域 global 上存在的对象,这里的标准指的是 ECMAcript 和其他环境(比如DOM)的标准。
const nums: Array<number> = [1,2,3]const date: Date = new Date()const err: Error = new Error('Error!');const reg: RegExp = /abc/;let body: HTMLElement = document.bodylet allDiv: NodeList = document.querySelectorAll('div');document.addEventListener('click', (e: MouseEvent) => { e.preventDefault() // Do something});](http://localhost:3000/)
我们使用react+ts实现一个todo List功能
功能包括:

https://create-react-app.dev/
yarn create react-app react-ts-demo --template typescriptcd react-ts-demo & yarn startexport interface ITodoItem { taskName: string, status: TaskStatus}// React.FC表示react的一个函数组件,是React.FunctionComponent的简写形式const TodoList:React.FC = ()=>{ const [list, setList] = useState<Array<ITodoItem>>([]) const [inputValue, setInputValue] = useState<string>("") const updateList = (list:ITodoItem[])=>{ setList(list) saveTaskList(list) } const handleAdd = ()=>{ if(!inputValue.trim()){ return alert("输入不能为空") } let [...newList] = list; newList.push({ taskName: inputValue, status: TaskStatus.PENDING }) updateList(newList) setInputValue("") } const handleStart = (index:number)=>{ let [...newList] = list; newList = newList.map((item,indx)=>{ if(indx === index){ return { ...item, status: TaskStatus.IN_PROGRESS } } return item }) updateList(newList) } const handleComplete = (index:number)=>{ let [...newList] = list; newList = newList.map((item,indx)=>{ if(indx === index){ return { ...item, status: TaskStatus.COMPLETED } } return item }) updateList(newList) } const handleDel = (index:number)=>{ let [...newList] = list; newList.splice(index,1) updateList(newList) } // 初始化列表数据 useEffect(() => { const list = getTaskList() setList(list) }, []) return ( <div className={styles.container}> <h1 className={styles.title}>TODO LIST</h1> <div className={styles.form}> <input className={styles.textInput} type="text" value={inputValue} placeholder="输入任务名" onChange={(e)=>setInputValue(e.target.value)}/> <button className={styles.addBtn} onClick={handleAdd}>添加任务</button> </div> <div className={styles.list}> {list.map((item,index)=> { return <TodoItem key={index} index={index} item={item} handleStart={handleStart} handleComplete={handleComplete} handleDel={handleDel}/> })} </div> </div> )}export default TodoList;interface IProps { index: number item: ITodoItem handleStart: (index: number) => void handleComplete: (index: number) => void handleDel: (index: number) => void}// 对于接收的组件参数,需要定义参数类型const TodoItem:React.FC<IProps> = ({index,item,handleStart,handleComplete,handleDel})=>{ return ( <div className={styles.taskItem}> <div className={styles.name}>{item.taskName}</div> <div className={styles.statusText}> {item.status===TaskStatus.PENDING && <span className={styles.pending}>待开始</span>} {item.status===TaskStatus.IN_PROGRESS && <span className={styles.inProgress}>进行中</span>} {item.status===TaskStatus.COMPLETED && <span className={styles.completed}>已完成</span>} </div> <div className={styles.btns}> {item.status===TaskStatus.PENDING && ( <button className={styles.btn} onClick={()=>handleStart(index)}>开始</button> )} {item.status===TaskStatus.IN_PROGRESS && ( <button className={styles.btn} onClick={()=>handleComplete(index)}>完成</button> )} <button className={styles.btn} onClick={()=>handleDel(index)}>删除</button> </div> </div> )}export default TodoItem我们在第一次进入的时候获取初始化数据
const getTaskList = ()=> { const taskList = localStorage.getItem("taskList") if(taskList){ return JSON.parse(taskList) } return []}// 初始化列表数据 useEffect(() => { const list = getTaskList() setList(list) }, [])添加、完成以及删除的时候保存和更新数据
const saveTaskList = (list: ITodoItem[])=>{ localStorage.setItem('taskList',JSON.stringify(list))}const updateList = (list:ITodoItem[])=>{ setList(list) saveTaskList(list) }上面代码我已经上传到github,欢迎star????