TypeScript是微软开发的一个开源的编程语言,通过在JavaScript的基础上添加静态类型定义构建而成。这意味着TypeScript是JavaScript的超集,它包含了JavaScript的所有元素,并为其添加了可选的静态类型和基于类的面向对象编程。
一、主要特性
- 类型批注:TypeScript允许开发者为变量、函数参数和返回值等添加类型批注,这有助于在编译时捕获类型错误,提高代码的健壮性。
- 声明文件:TypeScript支持为已存在的JavaScript库添加类型信息的头文件(.d.ts文件),这扩展了它对流行库的支持,如jQuery、MongoDB、Node.js等。
- 泛型:泛型允许开发者在定义函数、接口和类时不指定具体类型,而是在使用时指定,这提高了代码的可重用性和灵活性。
- 兼容性:TypeScript编译后的代码是纯JavaScript代码,可以在任何支持JavaScript的环境中运行,如浏览器、Node.js等。
二、核心知识点
- 基本类型:包括字符串(string)、数字(number)、布尔值(boolean)、数组(Array)、元组(tuple)、枚举(enum)等。
- 任意类型:使用any类型可以表示任意类型的值,但在使用时应尽量避免,以保持代码的类型安全性。
- 空值类型:void类型表示没有任何类型,通常用于表示函数没有返回值;null和undefined类型分别表示空值和未定义值。
- 函数:TypeScript中的函数支持默认参数、可选参数、剩余参数和箭头函数等特性。
- 类与接口:TypeScript支持类的定义和继承,以及接口的实现。类可以包含属性、方法、构造函数和修饰符等;接口用于定义对象的形状,可以包含属性、方法以及泛型约束等。
- 装饰器:装饰器是一种特殊类型的声明,它能够被附加到类声明、方法、访问器、属性或参数上。装饰器使用@expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息作为参数传入。
三、基本语法
1、变量声明和类型注解
对变量的类型限制,是先声明还是后注解,两种写法。
|
1 2 3 4 5 6 7 8 9 |
// js写法 let a = 1; // 变量声明 let b:number = 1; const c:boolean = true; // 类型注解 let d = <string>'hello'; |
2、函数和箭头函数
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 原方法 function add(a,b){ return a + b; } // ts声明限制 function add2(a:number,b:number){ return a + b; } // add2的返回值自动推断为number,但是我们也可以手动设置返回类型 function add2(a:number,b:number):number{ return a + b; } // 箭头函数同理 const arrowSum = (a:number,b:number):number => a + b; |
3、类型和接口
|
1 2 3 4 5 6 7 8 9 10 11 |
// 用type声明类型 type Point = { x:number, y:number } // 方法1:声明 let p:Point = {x:1,y:1} // 方法2:类型注解 let p2 = <Point>{x:1,y:1} |
接口定义对于java同学应该不陌生,它的使用和类型是一样的
|
1 2 3 4 5 6 7 8 9 10 |
// 接口定义 interface Person{ name: string, age: number } // 方法1:声明 let p3:Person = { name:'hello', age:12 } // 方法2:注解 let p4 = <Person>{ name:'hello', age:12 } |
通过类来实现接口
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Student implements Person{ name: string age: number constructor(name:string, age:number){ this.name = name; this.age = age; } } // Person,可以传入Student function info(person:Person){ console.log('我的名字:'+person.name) } const p5 = new Student('名字','18'); // info调用传入p5,因为Student是实现了Person接口的 info(p5); |
这里有个简化操作:我们可以把接口类型作为属性放到类里,所以上面的接口类可以改为
|
1 2 3 4 |
class Student implements Person{ constructor(public name:string, public age:number){} } |
接口也是可以继承的
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 定义Person2继承Person interface Person2 extends Person{ say(): void } // 用Person2 实现接口 class Student2 implements Person2{ constructor(public name:string, public age:number){} say(): void { console.log('这里我说了') } } // 同前面一样,调用 const p6 = new Student2('名字','18'); info(p6); |
interface可以定义属性,还可以定义函数
|
1 2 3 4 5 6 7 8 9 10 11 12 |
// 定义一个函数,参数是person interface Info{ (person:Person): void } // 定义一个函数来实现接口,此时入参person不需要显示指定类型,因为info2已经做了限制里 const info2: Info = (person) => { console.log('我的名字:'+person.name) } // 同前面一样,调用 const p7 = new Student2('名字','18'); info(p7); |
另外,在ts中是可以定义可选值的,只需要多加一个问号
|
1 2 3 4 5 6 7 |
interface Person3{ name: string, age?: number // 这里多了一个问号,表示有age的时候限制类型 } // 下面调用就可以不传age了(开发中不会报错) const p8: Person3 = {name:'我的名字'} |
4、命名空间与模块
命名空间是ts特有的,js中没有,关键字是namespace
|
1 2 3 4 5 6 7 8 9 |
namespace App{ const version = '1.0.0' export function getVersion(){ return version } } // 调用 App.getVersion(); // log一下能看到是1.0.0 |
在这里直接去拿version就不行,为什么呢,其实可以编译一下看它生成的结果
|
1 2 3 4 5 6 7 8 9 |
var App; (function (App){ const version = '1.0.0'; function getVersion(){ return version } App.getVersion = getVersion; })(App || (App={})) // 我们发现它生成了一个闭包 |
模块和命名空间类似,关键字是module,把上面的例子中的namespace改成module即可,并且编译结果也基本一致。那么这两者有什么区别呢?主要是跟场景有关,命名空间是为了集合某一类的操作,而模块一般用于npm包。
5、数组与元组
数组声明和常量类似,数组多了一个方括号
|
1 2 3 4 |
// 常量:let a:string = 'ccc'; const arr:string[] = ['aaa','bbb']; // arr的push、pop等方法,同js |
元组和数组是一样的,用法略有不同
|
1 2 3 |
const [name, age] = <[string, number]>['我的名字', 18]; // 你可以理解为对数组的每个元素都定义类型的方式 |
写过react的同学肯定用过这个 const [state, setState] = useState();其实useState()就是一个元组,返回了两个东西,第一个是值,第二个是设置状态的函数。
6、枚举
|
1 2 3 4 5 6 7 8 9 |
enum Colors { Red, Green, Blue } function printColors(color:Colors){ console.log(color) } // 调用 printColors(Colors.Red); |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
// 通过编译,我们发现枚举还是闭包 var Colors; (function (Colors){ Colors[Colors['Red'] = 0] = 'Red'; Colors[Colors['Green'] = 1] = 'Green'; Colors[Colors['Blue'] = 2] = 'Blue'; })(Colors || (Colors={})) printColors(Colors); // 输出 {'0': 'Red', '1':'Green', '2':'Blue', Red: 0, Green: 1, Blue: 2} // 枚举的本质是元素和索引互为键值对的,开发中可以用枚举代替魔法字符串。 |
7、特殊类型any、never、unknown
|
1 2 3 4 5 6 7 8 |
// 赋值可以是任何数据类型 let bb:any = 1; bb = true; // 这里更换类型也是可以 // 任意类型也可以赋值到其他类型的常量 let aa: number = 1; aa = bb; |
|
1 2 3 4 5 6 |
// never与any相反,如下,表示永远不会发生的值 function test(): never{ throw new Error(); // 第二种情况:while(true){},死循环 } |
|
1 2 3 4 5 6 7 8 |
// nuknown表示不知道的类型 let cc: nuknown = 1; cc = 'hello'; // 因为不知道类型,这里可以赋值为其他类型 // 和any的区别:nukown类型的数据不能赋值给其他类型 // 上面我们可以aa = bb; aa = cc; // 这个是不对的 |
四、高级特性
1、关键字
typeof在js中用来判断变量的类型,ts中也可以,但是ts还可以以定义的形式拿到类型;
|
1 2 3 |
let aa: number = 1; type BTtype = typeof aa; // 把拿到的类型放到一个type里,这时候的BTtype就是 number |
keyof是ts里的,用来获取对象的key
|
1 2 3 4 5 6 7 8 9 10 11 12 |
type Person = { name: string, age: number } type PersonKeys = keyof Person; // PersonKeys里就有了对象的所有key // 下面开始使用 const p: Person = {name: '姓名', age: 18} function getValueFromKey(key: PersonKeys){ return p[key] } console.log(getValueFromKey('name')); // 调用的时候就会有 name、age 的提示了(见末尾图) |
in在ts中和js都有
|
1 2 3 4 5 6 7 8 |
for(const key in Person){ console.log(key); // 这里js通过in遍历Person里的所有key } // ts中,通过in复制出新的类型Person2,同Person的键是一样的 type Person2 = { [key in PersonKeys]: string } |
extends在js中做来做继承
|
1 2 3 4 5 6 7 8 9 10 11 |
class a {}; class b extends a {} // 只不过在ts中,还可以用来做接口的继承 interface aa { a: number } interface bb extends aa{ b: string } const b1: bb = {a:1, b:'hello'} //这样在用到bb做类型声明的时候就需要同时满足a、b类型条件 |
is是ts特有的,用来定义类型保护函数
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function isString(value: nuknown){ return typeof value === 'string'; } // 判断正确会返回true,但是ts引擎是不知道的,可以这么改 function isString(value: nuknown): value is string{ return typeof value === 'string'; } // 举个例子 let nuknownType: unknown = Math.randow()>0因为.5 ? '1' : 1; //条件不同类型也不同 nuknownType.trim(); // 如果后面我们调用trim,类型未知,会有运行时错误 // 改成 if(isString(nuknownType)){ nuknownType.trim(); } |
as是ts特有的,用来做类型断言
|
1 2 3 4 5 6 |
// 上面的例子我们用isString做判断,但是也可用as断言 nuknownType = '中文名字'; nuknownType.trim(); // 这样还是报错,因为它的类型本身不确定,这里的string是我们自己确定 // 所以用as,告诉ts这就是一个string (nuknownType as string).trim(); // 编码和编译时候检查,而不会在运行时检查 |
const在js中很常见,在ts里还有另一个作用,定义静态枚举
|
1 2 3 4 |
const enum Colors { Red, Green, Blue } printColors(Colors); // 报错,因为const枚举在编译后不存在 printColors(Colors.Red); // 输出 0 |
readonly是ts特有的,可以把某个属性变成只读的
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
interface Row { id: number, name: string } const row: Row = { id: 1, name : '名字' } // 后面我们可以通过 row['id']=2来修改id,如果不想id被修改 interface Row { readonly id: number, name: string } |
publick和private用来定义公共属性和私有属性
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class Person{ constructor(public name:string, private idcard:string){} getName(){ console.log(this.name); } getIdcard(){ console.log(this.idcard); } } const he = new Person('姓名','11233qqq'); // he.name、he.getName()、he.getIdcard()都可以访问,但是 he.idcard //报错 // js中也可以设置私有属性,但是方法不同,先改一下上面方法,加入#money class Person{ #money:number; constructor(public name:string, private idcard:string){ this.#money = 100 } } he.#money // he后不会提示这个,强行写上运行会报错 // 总结:用#更安全,因为它表示不可外部访问,而private随便在开发或者校验时报错,但是本身可以访问到 |
2、泛型
为参数类型提供占位符,从而增加代码的灵活性和复用性。
函数泛型
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// 普通函数 function getVlueFromKey(obj, key){ return obj[key] } // 改装后,O和K就有了关联,并且限制了类型 function getVlueFromKey<O, K extends keyof O>(obj:O, key:K):O[K]{ return obj[key] } // 使用 const obj = { a: 1, b: 'hello', c: true, d: [1, 2] } getVlueFromKey(obj, 'a'); //我们在使用方法的时候会有提示它的value和key,并且后面.的时候会提示对应的类型的方法,这就是使用泛型后的作用 |
接口泛型
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 通常接口返回值是固定了,data不确定,用泛型 interface Apiresult<O> { code: number, msg: string, data: O } // 使用 async function request<D>(api: string): Promise<Apiresult<D>> { return await fetch(api).then(res => res.json)) as Apiresult<D> } request('').then(res=>{}) //request<string>('').then(res=>{ 这里就可以直接使用res.data.字符串方法 }) |
类泛型
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class ResponseResult<O> { constructor( publick readonly code:number, msg: string, public readoly data:O ){} } // 使用 async function request<D>(api: string): Promise<ResponseResult<D>> { return await fetch(api).then(res => res.json)) as ResponseResult<D> } request<string>('').then(res=>{ 这里就可以直接使用res.data.字符串方法 }) // 可以看出调用和上面的接口泛型是一样的 |
类型别名的泛型
|
1 2 3 4 5 6 7 |
type ResultType<O> { code: number, msg: string, data: O } // 它的使用同上面,把Pormise里的泛型 ResponseResult 改成 ResultType 即可 |
|
1 2 3 4 5 6 7 8 |
// 泛型的默认类型是unknown,但是我们可以给它添加默认类型 interface Apiresult<O = object> { code: number, msg: string, data: O } |
3、常用内置泛型
Partical:手动把对象里字段改成可选;
|
1 2 3 4 5 6 |
interface Person = { name: string, age: number } let xiaoming: Partical<Person> = {} |
Required:把对象里的字段改成必选项;
ReadOnly:把对象里的字段都改成只读的;
Recode:声明一个纯对象,属性名的类型由泛型参数决定,值由另一个泛型参数决定;
Pick:从一个对象中挑出指定的属性;
Omit:从一个对象中删除指定的属性;
…还有更多其他内置泛型,可以自行去看,可以帮助我们开发中减少很多代码量
4、高级类型
交叉类型
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
interface A1 { a: string } type B1 { b: number } class C1 { constructor(public c: boolean) } type ABC = A1 & B1 & C1; // 使用 const abc: ABC{ a: 'hello', b: 1, c: false } |
联合类型
|
1 2 3 4 5 6 7 |
// 还是上面的 type ABC2 = A1 | B1 | C1; // 使用 const abc2: ABC2 { b:12 } // 联合类型赋值的时候,必须是被联合类型里的 |
类型保护
|
1 2 3 4 5 6 7 |
// 如果函数入参接受一个联合类型,实际使用的时候怎么判断其对应哪个类型呢? function formateData(input: string | Data | number){ // 这里要返回格式化后的字符串,参数不确定 // typeof判断类型,对应去做处理,比如是number就toString后返回 // 我们可以使用前面写的isString } |
五、注意事项
- 代码启用严格模式,让ts编译器严格检查代码,避免出错,代码更健壮;
- 避免使用any类型,虽然方便使用,但是失去了代码提示的能力,多参数可以用联合类型;
- 避免类型断言的滥用,不判断就直接断言,可能会有程序崩溃;
- 使用readonly属性和参数,保护属性,避免误操作;
- 遵循命名约定;
- 使用类型检测工具,对命名和规范进行强约定;
- 编写清晰的文档和注释,好的注释能让代码更易读易懂;
- 测试用例,出入参数覆盖比例越大,函数或方法越健壮,程序也越稳定。
使用场景
TypeScript非常适合用于大型项目的开发,因为它提供了静态类型检查、代码自动补全、重构等特性,有助于提高开发效率和代码质量。此外,TypeScript还支持为现有的JavaScript库添加类型信息,使得在开发过程中能够更好地利用这些库。
下面有几张图,我们来看下TS在开发过程中对类型检查、代码提示、自动补全等方面的一个效果:




