首页 » 前端 » 正文

typesciprt学习笔记

TypeScript是微软开发的一个开源的编程语言,通过在JavaScript的基础上添加静态类型定义构建而成。这意味着TypeScript是JavaScript的超集,它包含了JavaScript的所有元素,并为其添加了可选的静态类型和基于类的面向对象编程。

一、主要特性

  1. 类型批注:TypeScript允许开发者为变量、函数参数和返回值等添加类型批注,这有助于在编译时捕获类型错误,提高代码的健壮性。
  2. 声明文件:TypeScript支持为已存在的JavaScript库添加类型信息的头文件(.d.ts文件),这扩展了它对流行库的支持,如jQuery、MongoDB、Node.js等。
  3. 泛型:泛型允许开发者在定义函数、接口和类时不指定具体类型,而是在使用时指定,这提高了代码的可重用性和灵活性。
  4. 兼容性:TypeScript编译后的代码是纯JavaScript代码,可以在任何支持JavaScript的环境中运行,如浏览器、Node.js等。

二、核心知识点

  1. 基本类型:包括字符串(string)、数字(number)、布尔值(boolean)、数组(Array)、元组(tuple)、枚举(enum)等。
  2. 任意类型:使用any类型可以表示任意类型的值,但在使用时应尽量避免,以保持代码的类型安全性。
  3. 空值类型:void类型表示没有任何类型,通常用于表示函数没有返回值;null和undefined类型分别表示空值和未定义值。
  4. 函数:TypeScript中的函数支持默认参数、可选参数、剩余参数和箭头函数等特性。
  5. 类与接口:TypeScript支持类的定义和继承,以及接口的实现。类可以包含属性、方法、构造函数和修饰符等;接口用于定义对象的形状,可以包含属性、方法以及泛型约束等。
  6. 装饰器:装饰器是一种特殊类型的声明,它能够被附加到类声明、方法、访问器、属性或参数上。装饰器使用@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
}

publickprivate用来定义公共属性和私有属性

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
}

五、注意事项

  1. 代码启用严格模式,让ts编译器严格检查代码,避免出错,代码更健壮;
  2. 避免使用any类型,虽然方便使用,但是失去了代码提示的能力,多参数可以用联合类型;
  3. 避免类型断言的滥用,不判断就直接断言,可能会有程序崩溃;
  4. 使用readonly属性和参数,保护属性,避免误操作;
  5. 遵循命名约定;
  6. 使用类型检测工具,对命名和规范进行强约定;
  7. 编写清晰的文档和注释,好的注释能让代码更易读易懂;
  8. 测试用例,出入参数覆盖比例越大,函数或方法越健壮,程序也越稳定。

使用场景

TypeScript非常适合用于大型项目的开发,因为它提供了静态类型检查、代码自动补全、重构等特性,有助于提高开发效率和代码质量。此外,TypeScript还支持为现有的JavaScript库添加类型信息,使得在开发过程中能够更好地利用这些库。

下面有几张图,我们来看下TS在开发过程中对类型检查、代码提示、自动补全等方面的一个效果: