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、变量声明和类型注解
对变量的类型限制,是先声明还是后注解,两种写法。
// js写法
let a = 1;
// 变量声明
let b:number = 1;
const c:boolean = true;
// 类型注解
let d = <string>'hello';
2、函数和箭头函数
// 原方法
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、类型和接口
// 用type声明类型
type Point = {
x:number,
y:number
}
// 方法1:声明
let p:Point = {x:1,y:1}
// 方法2:类型注解
let p2 = <Point>{x:1,y:1}
接口定义对于java同学应该不陌生,它的使用和类型是一样的
// 接口定义
interface Person{
name: string,
age: number
}
// 方法1:声明
let p3:Person = { name:'hello', age:12 }
// 方法2:注解
let p4 = <Person>{ name:'hello', age:12 }
通过类来实现接口
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);
这里有个简化操作:我们可以把接口类型作为属性放到类里,所以上面的接口类可以改为
class Student implements Person{
constructor(public name:string, public age:number){}
}
接口也是可以继承的
// 定义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可以定义属性,还可以定义函数
// 定义一个函数,参数是person
interface Info{
(person:Person): void
}
// 定义一个函数来实现接口,此时入参person不需要显示指定类型,因为info2已经做了限制里
const info2: Info = (person) => {
console.log('我的名字:'+person.name)
}
// 同前面一样,调用
const p7 = new Student2('名字','18');
info(p7);
另外,在ts中是可以定义可选值的,只需要多加一个问号
interface Person3{
name: string,
age?: number // 这里多了一个问号,表示有age的时候限制类型
}
// 下面调用就可以不传age了(开发中不会报错)
const p8: Person3 = {name:'我的名字'}
4、命名空间与模块
命名空间是ts特有的,js中没有,关键字是namespace
namespace App{
const version = '1.0.0'
export function getVersion(){
return version
}
}
// 调用
App.getVersion(); // log一下能看到是1.0.0
在这里直接去拿version就不行,为什么呢,其实可以编译一下看它生成的结果
var App;
(function (App){
const version = '1.0.0';
function getVersion(){
return version
}
App.getVersion = getVersion;
})(App || (App={})) // 我们发现它生成了一个闭包
模块和命名空间类似,关键字是module,把上面的例子中的namespace改成module即可,并且编译结果也基本一致。那么这两者有什么区别呢?主要是跟场景有关,命名空间是为了集合某一类的操作,而模块一般用于npm包。
5、数组与元组
数组声明和常量类似,数组多了一个方括号
// 常量:let a:string = 'ccc';
const arr:string[] = ['aaa','bbb'];
// arr的push、pop等方法,同js
元组和数组是一样的,用法略有不同
const [name, age] = <[string, number]>['我的名字', 18];
// 你可以理解为对数组的每个元素都定义类型的方式
写过react的同学肯定用过这个 const [state, setState] = useState();
其实useState()就是一个元组,返回了两个东西,第一个是值,第二个是设置状态的函数。
6、枚举
enum Colors {
Red, Green, Blue
}
function printColors(color:Colors){
console.log(color)
}
// 调用
printColors(Colors.Red);
// 通过编译,我们发现枚举还是闭包
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
// 赋值可以是任何数据类型
let bb:any = 1;
bb = true; // 这里更换类型也是可以
// 任意类型也可以赋值到其他类型的常量
let aa: number = 1;
aa = bb;
// never与any相反,如下,表示永远不会发生的值
function test(): never{
throw new Error();
// 第二种情况:while(true){},死循环
}
// nuknown表示不知道的类型
let cc: nuknown = 1;
cc = 'hello'; // 因为不知道类型,这里可以赋值为其他类型
// 和any的区别:nukown类型的数据不能赋值给其他类型
// 上面我们可以aa = bb;
aa = cc; // 这个是不对的
四、高级特性
1、关键字
typeof
在js中用来判断变量的类型,ts中也可以,但是ts还可以以定义的形式拿到类型;
let aa: number = 1;
type BTtype = typeof aa; // 把拿到的类型放到一个type里,这时候的BTtype就是 number
keyof
是ts里的,用来获取对象的key
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都有
for(const key in Person){
console.log(key); // 这里js通过in遍历Person里的所有key
}
// ts中,通过in复制出新的类型Person2,同Person的键是一样的
type Person2 = {
[key in PersonKeys]: string
}
extends
在js中做来做继承
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特有的,用来定义类型保护函数
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特有的,用来做类型断言
// 上面的例子我们用isString做判断,但是也可用as断言
nuknownType = '中文名字';
nuknownType.trim(); // 这样还是报错,因为它的类型本身不确定,这里的string是我们自己确定
// 所以用as,告诉ts这就是一个string
(nuknownType as string).trim(); // 编码和编译时候检查,而不会在运行时检查
const
在js中很常见,在ts里还有另一个作用,定义静态枚举
const enum Colors { Red, Green, Blue }
printColors(Colors); // 报错,因为const枚举在编译后不存在
printColors(Colors.Red); // 输出 0
readonly
是ts特有的,可以把某个属性变成只读的
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
用来定义公共属性和私有属性
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、泛型
为参数类型提供占位符,从而增加代码的灵活性和复用性。
函数泛型
// 普通函数
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,并且后面.的时候会提示对应的类型的方法,这就是使用泛型后的作用
接口泛型
// 通常接口返回值是固定了,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.字符串方法 })
类泛型
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.字符串方法 })
// 可以看出调用和上面的接口泛型是一样的
类型别名的泛型
type ResultType<O> {
code: number,
msg: string,
data: O
}
// 它的使用同上面,把Pormise里的泛型 ResponseResult 改成 ResultType 即可
// 泛型的默认类型是unknown,但是我们可以给它添加默认类型
interface Apiresult<O = object> {
code: number,
msg: string,
data: O
}
3、常用内置泛型
Partical:手动把对象里字段改成可选;
interface Person = {
name: string,
age: number
}
let xiaoming: Partical<Person> = {}
Required:把对象里的字段改成必选项;
ReadOnly:把对象里的字段都改成只读的;
Recode:声明一个纯对象,属性名的类型由泛型参数决定,值由另一个泛型参数决定;
Pick:从一个对象中挑出指定的属性;
Omit:从一个对象中删除指定的属性;
…还有更多其他内置泛型,可以自行去看,可以帮助我们开发中减少很多代码量
4、高级类型
交叉类型
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
}
联合类型
// 还是上面的
type ABC2 = A1 | B1 | C1;
// 使用
const abc2: ABC2 {
b:12
} // 联合类型赋值的时候,必须是被联合类型里的
类型保护
// 如果函数入参接受一个联合类型,实际使用的时候怎么判断其对应哪个类型呢?
function formateData(input: string | Data | number){
// 这里要返回格式化后的字符串,参数不确定
// typeof判断类型,对应去做处理,比如是number就toString后返回
// 我们可以使用前面写的isString
}
五、注意事项
- 代码启用严格模式,让ts编译器严格检查代码,避免出错,代码更健壮;
- 避免使用any类型,虽然方便使用,但是失去了代码提示的能力,多参数可以用联合类型;
- 避免类型断言的滥用,不判断就直接断言,可能会有程序崩溃;
- 使用readonly属性和参数,保护属性,避免误操作;
- 遵循命名约定;
- 使用类型检测工具,对命名和规范进行强约定;
- 编写清晰的文档和注释,好的注释能让代码更易读易懂;
- 测试用例,出入参数覆盖比例越大,函数或方法越健壮,程序也越稳定。
使用场景
TypeScript非常适合用于大型项目的开发,因为它提供了静态类型检查、代码自动补全、重构等特性,有助于提高开发效率和代码质量。此外,TypeScript还支持为现有的JavaScript库添加类型信息,使得在开发过程中能够更好地利用这些库。
下面有几张图,我们来看下TS在开发过程中对类型检查、代码提示、自动补全等方面的一个效果:




