Huifeng Jiao
jLab

jLab

TypeScript快速学习笔记(上)

Huifeng Jiao's photo
Huifeng Jiao
·Nov 11, 2022·

4 min read

Subscribe to my newsletter and never miss my upcoming articles

说明

  • 强类型语言TS,实际上一直在用,虽然用的磕磕绊绊,不过这次因为是钱包,感觉类型等安全问题要重视,再学习一遍。
  • 学习Up主的一个学习视频,bilibili.com/video/BV1XG411E7dg/?p=5&sp..,他学习的是ORelly的一本书。

  • 《Learning TypeScript Enhance Your Web Development Skills Using Type-Safe JavaScript》,Josh Goldberg,O' Reilly,2022

  • 书的源代码:learningtypescript.com/from-javascript-to-t..

  • 以下是笔记,计划后面有点经验和体感了,给不熟悉前端的后端同学开个快速入门课程。

1 JS to TS

类型系统提示代码安全和稳定性

类型检查器分析代码

仅在开发环境下的类型注释:何时添加

TS同IDE结合重构代码

TS内建推断和新语法来表示复杂类型

95年Brendan Eich,10天设计了JS

Show me the best language, I'll show you the no users language---Anders Hejlsberg (TS createor)

JS问题

类型没有上下文,容易包含编译报错陷阱,没有强类型检查

JsDoc文档对代码无约束切重构要重写,结构松散

配套工具较弱

TS优势

2010年创建,12年开源,Anders Hejlsberg 创建(大牛)

四部分:编程语言(JS+特殊类型语法)、类型检查器、编译器(报告问题+输出等价JS)、语言服务(协调编译器工作)

开始

node --version
npm install --location=global typescript (failed with error)

 npm i -g typescript (it is ok!)
 npm install -g typescript(the same)

tsc --version

具体项目目录
tsc --init
生成tsconfig.json

编译:tsc index.ts

其他:

npx http-server 可以一句话帮你开启一个静态服务器!
npx 会自动查找当前依赖包中的可执行文件,如果找不到,就会去 PATH 里找。如果依然找不到,就会帮你安装!

其他

TS不会和JS冲突

TS编译为JS需要时间,Babel可以更快编译?

2 类型系统

TS基础类型7种

null
undefined
boolean
string
number
bigint
symbol

类型系统:代码中数据类型规则的体系,成为类型系统

TS类型系统:

理解代码所有值的类型

观察初始值,推断可能包含什么类型

观察每个值,在代码中的所有用法

如果值和类型不匹配,报错提示


返回ts代码两种错误类型:

语法错误,写错了,词不达意

类型错误,方法或者值与类型不匹配(可赋值性 assignability+方法属性)

如果没有那些属性,则ts会报错,成为类型形状 type shapes

类型推断不匹配的报错,可以显式说明变量类型,成为类型注释

let body_weight: number = 49; 
使用类型注释解决类型推断不符合预期的问题
避免变量没有初始值和类型,因为会被隐式推断为any类型,是风险

let birthplace: string;
birthplace = true;  (会报错提示)

无法推断类型的根据赋值自动识别为对应类型,也具备了对应类型的方法


约定

同目录下多个ts文件

包含import或者export语句,则该文件是一个模块 module

不包含上述的,就是普通ts代码,是script

变量名冲突

1.模块内变量名的冲突,根据是否有import及import的变量名(别名)是否和本模块相同,相同则冲突。

2.不同脚本中存在同名变量,会产生冲突,可tsconfig中配置更改冲突规则。

3 联合和字面量?

Unions: 允许两个多多个类型的赋值

let mathematician = Math.random()>0.5 ? "祖冲之" : undefined;
let thinker: string|null;
这种变量因为可能是两种类型,则访问该变量方法,会排除单个类型的方法,保留共有属性方法

narrowing:将值允许的类型减少为不是一个或多个可能的类型?

类型守卫代码<type guard>,指示ts此处(联合类型)的具体类型,就可以访问该类型独有的属性,这个过程叫narrowing.
有几种方式收缩:
给一个确定类型的初始值
对变量的具体值进行判断
对变量值的类型进行判断

字面量 literals

一般是指const固定的类型,固定了变量类型和值,具有对应类型的属性方法

这个是联合类型混合了字面量类型的变量:
let score: number|"不及格"|"及格"|"良好"|"优秀";

虚值

falsy: false 0 -0 0n "" null undefined NaN

真值

不等于虚值的就是真值

严格空值检查

Tsconfig.json中的 strictNullChecks = true ,则null和undefined不能赋值给其他类型

类型别名

type关键字可以给某类型一个别名,如果它很长的话,常用大驼峰命名,可在任意处定义,存在即可。

type RawData = boolean | number | string |null |undefined;

let raw_data_first: RawData = false;

type IDMabe = Id | undefined | null;
type Id = number | string;

4 对象

对象字面量

对象类型匹配对应的属性和属性类型,缺少/增加属性,属性类型不匹配,都会产生类型错误。

变量/属性+?表示可选

let singer: {
name: string,
age?: number,
work_title: string|undefined,
} = {
name: "张纪中",
// age属性可以不存在
work_title: "导演" // work_title 必须存在,可以是undefined。
};

1.使用字面变量赋值时,必须严格满足对象类型要求,不能有额外属性。

2.而将对象赋值给变量,则只要求该对象包含变量类型要求的属性,对象有额外属性不会报错。

type WithFirstName = {first_name: string;};
type WithLastName = {last_name: string;};

const has_both = {
first_name: "Lucas",
last_name: "Clif"
};

let with_first_name: WithFirstName = has_both;
变量被赋值一个对象,只要对象有变量类型的一个属性,则可以赋值

const has_both_error: WithFirstName = {
    first_name: "Lucas",
    last_name: "Clif"  //这里编译器会报错,因为变量被赋值的是字面量,则要求属性完全匹配
};

ps:错误的代码编译为js后,可以正常输出,js的容忍度很高啊

推断的联合对象类型

不是共有属性的,在单个类型中以可选属性表示,属性类型是undefined

let best_song = Math.random()>0.5 ? {
    name: "song1",
    single: true,
    duration: "4:08"
} : {
    name:"song2",
    single: "是",
    release: new Date(2022, 0, 20)
};

显式的联合对象类型,非共有属性,不可直接访问,需要narrowing。

if in判断属性是否存在
或者增加判断属性来访问判断

intersection类型

更像是满足多个类型的类型,而不是满足多个类型的共有部分?

let one_written_art: Artwork & Writing = {
    enre: "genre",
    pages: 100,
    name: "name"
}

type Artwork = {
    genre: string;
    name: string;
};

type Writing ={
    pages: number;
    name: string;
};

多个类型interset时,某个属性名相同单类型不同时,这个属性就是never类型

never类型表示不可能出现在代码的类型状态。

5 函数

函数参数

parameter,定义的参数,形式参数,行参

argument,传递的参数,实际参数,实参

形参:type

形参?: type,表示可选参数,可以有多个

形参 = default value,表示默认参数,不用标记类型,自动根据值判断类型

其余参数定义: function sing_all_songs(singer: string, ...songs: string[]) {}

实参直接值,类型与形参类型相同,数量也相同


作为返回类型: ts根据返回值判断返回类型

1.多个return语句(活return结果可能不同)的函数,返回值类型是每个return结果的联合类型

2.显式返回值类型: 用箭头函数,后面是返回值类型 function aaa(aa: string, bb=0): number {}

显式返回可以提升类型检查速度,不必推断,或有必要强制返回值类型时


函数类型:写法和箭头函数类似,知识返回值变为返回值类型,function aaa(v: string) => string {}

void函数返回值类型表示返回nothing,undefined不匹配void类型

never返回值类型表示never return,用于总抛出错误或者在无限循环结构的函数。


函数重载 overload

因为传入参数形式不同而执行不同动作,即函数有多个版本,成为函数重载。

1.先重载签名,多次声明函数,函数名相同,参数形式和返回值不同,不写函数体

2.然后实现重载签名,完成函数体内容,记住要兼容所有重载签名才可以

重载是最后手段,ts编译为js后,重载签名会被清除


6 数组

数组和元组(array和tuple)

type [] or [values],依靠值判断类型

let variance: number[];

Variance = [41, -7, 0, 6.4];

联合数组类型:type array_of_string_or_numbers = (string | number)[];

避免any类型的数组,例如let aa = [];数组类型会不断根据值而变化

let three_demesional : boolean[][][][];
let complex: ((Symbo[] | bigint[])[])[];

TS的默认数组类型认为访问数组元素时一定能访问到,及时超出index,有开关可改变。

扩展运算:合并数组,不同类型将产生联合类型

剩余参数类型:...

const aa = [pp"","rr","ss"];
const bb = [4,5,8,9];
const joined_array = [...aa, ...bb];

function bubble(message: string, ...stuff: string[]){}

let abc: [string, number]; //注意:这个是元组,不是数组,数组是 let abc: (string | number)[];

优先判断为数组类型而非元组

显式注释元组类型: let m_tuple: [bigint, number] = [0n, 0];

在数字字面量后写上常量断言(as const): const readonly_tuple = [0n, 0] as const;

7 接口

做自己的类型,超越内置类型

接口:描述对象的形状,和对象类型的别名很类似。

1.提供易于阅读的错误信息

2.很快的编译速度

3.与类的交互性很好

//类型别名
type Book = {
title: string;
pages: number;
};

//等效的接口
interface IBook {
    title: string;
    pages: number;
};

let loved_book: IBook = {
    title: "Master C#",
    pages: 705
}

接口和类型别名区别:

1.接口可以合并(相同名字的接口)以增强功能,在协同第三方代码,如npm包很有用

2.接口可以描述类(class)的结构,类型别名不行

3.接口容易在checker内缓存,有更快的类型检查速度,类型别名本质是粘贴了对象类型的字面量。

4.接口被视为命名的对象,而不是一个未命名的字面量的别名,有更易阅读的错误信息

接口属性类型

attribute?:,表示可选属性

Readonly attribute, 表示只读属性(编译为JS此属性会失效)

接口函数和方法

方法语法:Member(): void;

属性语法:Member: () => void;

staff.member(); // 以上两种定义这里都正确

方法与函都支持可选修饰符,方法与函数大多数情况下是等同的,可交换。

二者区别:

1.方法不可以被readonly修饰

2.接口合并时,对方法和函数的处理不同

3.一些类型操作,对待方法和函数不同

如果你知道函数可能使用this,那用方法语法,否则可以使用属性语法

调用签名

一个值可以像函数一样调用,若函数有特殊的内部属性,可以调用签名来描述

没太看懂必要性?

索引签名

定义接口太多了就可以索引

你想存储键值时,key是位置的,用Map更安全,因为包含了undefined联合类型

key的类型只能是string, number, symbol

接口嵌套

interface Novel {
    author: {
        name: string;
        age?: number;
    },
    setting: Setting;
}
interface Setting {    
    place: string;
    readonly year: number;
}

let hovel: Novel = {
    author: {
        name: "jjj"
    },
    setting: {
        place: "England",
        year: 1199
    }
};

接口扩展(派生)

一个接口可以扩展另外一个接口,派生接口复制基础接口所有成员,还可以添加自己的新成员,让代码复用。

interface Writing {
    title: string;
    review?:(() => void) | boolean[];
    publish?(): symbol;
}

interface Novella extends Writing {
    pages: number; //新增属性
    review: boolean[]; //覆盖属性,保障类型匹配
    pulibsh?: () => bigint; //复制属性,类型不对
}

扩展多个属性

interface Avert extends Revert, Invert {
    ...
}

合并接口

合并的接口要同名,同一作用域。

一般为了增强功能,比如自定义一个接口来合并内置的全局接口,比如Window,或者外部包的接口

合并接口的属性类型必须一模一样

8 类

TS对待方法和构造器就像对待函数,允许声明参数类型、可选参数、参数默认值、剩余参数和函数返回值。

类的属性必须显式声明,不会从构造器的赋值来反推有哪些属性。

两种语法,让类中成员能像函数一样调用:

1.方法语法,创建的函数叫方法,不同实例访问的方法都是同一个函数。

2.属性语法,创建一个函数赋值给属性,不同实例有自己的属性,属性语法创建的函数是相互独立的。

初始化检查

TS默认开启严格的属性初始化检查:

1.属性类型不包含undefined

2.属性值没有初始值

3.没有在构造器为此属性复制

那TS会抱怨这个属性的不确定性,当然,属性名后面加!, attribute!: string;

就跳过了严格检查,表明第一次使用前一定会赋值。

可选属性在初始化检查中会跳过

只读属性指南在声明初始化或者构造器中初始化,不能在类方法、属性或者实例中初始化

类作为类型

一个对象结构满足类的要求,及时该对象不是类的实例,该对象也能匹配类(就是类的类型)

因为TS结构化检查只关心形状,不关心对象如何声明。

类与接口

类能够实现接口,让类实例遵循接口的类型要求

类实现接口,不改变类的使用,不会复制接口的任何成员到类中??,TS不会跟军接口来推断类成员类型

接口只是起到纯净的安全性检查作用,允许类实现多个接口,不冲突即可

当接口被设计去让类实现时,接口的函数应该使用方法语法定

不能同时满足两个接口的要求,智能实现其中一个

类的扩展

类能像接口那样扩展,子类拥有所有基类的属性和方法

子类没有写自己的构造器,则会隐式调用积累构造器。

子类如果有自己构造器,则必须通过super()调用基类构造器。

子类必须调用基类构造器,才可以访问this,super。最好把super()写在构造器的顶层语句。(前后为鸡和蛋了。。。)

覆盖方法/属性

方法可覆盖,单参数类型要匹配,返回值类型要匹配,匹配指兼容,非复制

属性同样,可覆盖,保持类型匹配,兼容

抽象类

抽象类是一个基类,不说明实现,值描述结构,其他类扩展抽象类后,可以实例化,抽象类不可实例化。

类名或方法前加 abstact即可

成员可见性

Public 默认的,允许任何人,任何地点访问

Protected, 允许在类本身和其子类访问

Private,允许类本身访问

#,允许在类本身访问??有啥区别?#是JS原版的,其他关键字是TS加的,#是原始私有成员的标志

#不能与其他TS可见性关键字混用,可以和readonly,static混用;readonly与可见性关键字可以混用

注意static静态属性访问需要类名而不是this来访问

实例不能访问protected和private

注意static修饰符,表示该字段/成员只能被类本身(不是在类本身中)访问,例如类名.static变量

顺序:可见性关键字 static readonly

Did you find this article valuable?

Support Huifeng Jiao by becoming a sponsor. Any amount is appreciated!

Learn more about Hashnode Sponsors
 
Share this