写在开头
此文尽可能全面地聚焦基础知识,进阶内容较少,如有需要可针对基础知识自行学习(或者我更😭)
字典式笔记,无清晰逻辑线,配合目录食用更佳~
没有涉及到的部分后面也会持续更新~
简介
JavaScript
基本简介:
- TypeScript 可以看成是 JavaScript 的超集,它继承了后者的全部语法,增加了一些自己的语法
- TypeScript 对 JavaScript 添加的最主要部分,就是一个独立的类型系统
- 在语法上,JavaScript 属于动态类型语言,而 TypeScript 属于静态类型语言,各有优缺点
- 因此,在 TypeScript 中进行以下行为都会报错:
- 随意给变量赋其他类型的值
- 随意增删对象属性
- 在给变量前赋值前调用变量
tsc 常用选项
JavaScript
TypeScript 的编译:
- JavaScript 的运行环境(浏览器和 Node.js)不认识 TypeScript 代码
- TypeScript 官方只提供编译器 (tsc) ,编译为能运行的 JavaScript 代码
- 因此,TypeScript 只是编译时的类型检查,而不是运行时的类型检查,编译后运行中不再检查
tsconfig. json
- TypeScript 允许将tsc的编译参数,写在配置文件tsconfig.json
- 只要当前目录有这个文件,tsc就会自动读取,所以运行时可以不写参数
tsc 的一些编译选项
JavaScript
1. ---outFile
将多个 TypeScript 脚本编译成一个 JavaScript 文件
$ tsc file1.ts file2.ts --outFile app.js
2. ---outDir
编译结果默认都保存在当前目录,--outDir参数可以指定保存到其他目录
$ tsc app.ts --outDir dist
3. ---target
tsc 默认将代码编译为很久的 JavaScript,即3.0版本(es3)
-target 可以指定编译后的 JavaScript 版本,建议使用es2015或者更新版本
$ tsc --target es2015 app.ts
4. --noEmitOnError
tsc 编译报错也会产生 JavaScript 产物
如果希望报错就停止编译,不生成编译产物,可以使用--noEmitOnError参数
$ tsc --noEmitOnError app.ts
5. --noEmit
只检查类型是否正确、是否有编译错误,不生成 JavaScript 文件
$ tsc --noEmit app.ts
6. --noImplicitAny
只要推断出 any 类型就会报错
$ tsc --noImplicitAny app.ts
例外: var x; let y; // 不报错,TypeScript 推断它们的类型为any
因此,使用 let 和 var 声明变量时,如果不赋值,最好显式声明类型,否则可能存在安全隐患
7. --strictNullChecks
启用后,undefined 和 null 无法赋值给 any 和 unknown 以外其他类型的变量(也无法相互赋值)
没有声明类型的变量被赋值为 undefined 或 null,可以被编译器正常推断为自身,而非 any
[类型系统] 类型分类
基本类型 | 数组 | 元组 | 对象 | 值
JavaScript
基本类型:
number、boolean、string、void、bigint、symbol、null、undefined、any、unknown、never
其中:
bigint:与 number 不兼容,不可赋值为整数和小数,ES2020 标准引入,编译参数target不得低于es2020
any:任何类型,允许赋予任何类型的值,属于"顶层类型"(top type)
污染:容易污染其它变量,导致编译时不出错,但运行时出错
unknown:不能直接使用,必须经过类型检查才能使用,可以视为"严格的any",也属于"顶层类型"(top type)
解决污染:不能赋值给 any 和 unknown 以外类型的变量,不能直接调用该类型变量的方法和属性,
不能直接当做函数执行,必须事先缩小类型
never:表示永远不会有值的类型,例如抛出异常或死循环的函数返回类型,属于"底层类型"(bottom type)
集合论: never 类型可以赋值给任意其他类型,因为空集是任何集合的子集
let something: any = 42;
let value: unknown = "Hello unknown";
something = "Hello any";
if (typeof value === "string") {
console.log(value.toUpperCase()); // 可以安全访问
}
function throwError(): never {
throw new Error("An error occurred");
}
其他特点:
JavaScript
1. 如果没有声明类型的变量被赋值为 undefined 或 null,它们的类型会被推断为 any
undefined 和 null 也可以赋给任何类型的值
在 tsc 打开 strictNullChecks 后,他们才可以被正常推断为自身,同时限制只能赋给 any 或 unknown
2. 包装对象 Boolean、String、Number、BigInt、Symbol
大写类型同时包含包装对象和字面量,小写类型只包含字面量
注意: 建议只使用小写类型,因为绝大部分使用原始类型的场合,都是使用字面量,不使用包装对象
而且,TypeScript 把很多内置方法的参数都定义成了小写类型,使用大写类型会报错
数组:
JavaScript
- 数组:可以通过类型注解来指定数组元素的类型,例如 number[] 或 Array<number>
let numbers: number[] = [1, 2, 3];
let otherNumbers: Array<number> = [1, 2, 3];
-- [数组类型特点]
1. 允许使用方括号读取数组成员的类型
type Names = string[];
type Name = Names[0]; // string
2. 成员数量可以动态变化,编译时不会对数组边界进行检查,越界访问数组并不会报错
let arr:number[] = [1, 2, 3];
let foo = arr[3]; // 不存在,但不报错
3. TypeScript 会推断空数组的类型是 any[],后面赋值时会自动更新类型推断
const arr = []; // 推断为 any[]
arr.push(123); // 推断类型为 number[]
arr.push('abc'); // 推断类型为 (string | number)[]
4. 在数组类型前面加上 readonly 关键字或使用 const 断言可声明只读数组,它是数组的父类型
let a1:number[] = [0, 1]; // 或 const arr = [0, 1] as const;
let a2:readonly number[] = a1; // 不报错
a1 = a2; // 报错
注意: readonly 关键字不能与数组的泛型写法一起使用,但 TypeScript 提供了两个专门的泛型用于只读数组
const a1:ReadonlyArray<number> = [0, 1];
const a2:Readonly<number[]> = [0, 1];
5. 多维数组,表示形式为 T[][]的形式,其中 T 是最底层数组成员的类型
var multi:number[][] = [[1,2,3], [23,24,25]];
元组:
JavaScript
- 元组:与数组类似,但固定长度,且可以包含不同类型的元素,例如 [string, number]
let person: [string, number] = ["John", 25];
-- [数组类型特点]
1. 使用元组必须明确给出类型声明,否则 TypeScript 会自动将其推断为数组
let a = [1, true]; // a 的类型为 (number | boolean)[]
对象:
JavaScript
- 对象: 显式指定一个对象的结构
let point: { x: number, y: number } = { x: 10, y: 20 };
-- [对象类型其他特点] Object 与 object
Object 代表 JS 广义对象,表示所有可以转成对象的值,可简写为{}
除了 undefined 和 null 以外的任何值都可以转为对象并赋值给 Object 类型,太广泛了,不易用
object 代表 JS 狭义对象,即可以用字面量表示的对象,只包含对象、数组和函数,不包括原始类型的值
大多数时候使用对象都不希望包含原始类型,所以建议总是使用object
注意: Object 和 object 都只包含 JS 内置对象原生的属性和方法,不包含用户自定义的属性和方法
值类型:
JavaScript
- 值类型
TypeScript 遇到 const 命令声明且未标注类型的变量,会推断其为值类型
单个值类型意义不大,实际开发往往将多个值结合,作为联合类型使用
注意: const命令声明的变量,如果赋值为对象,并不会推断为值类型
毕竟 JavaScript 里 const 变量赋值为对象时,属性值也是可以改变的
const x = { foo: 1 }; // x 的类型是 { foo: number }
接口 (Interfaces)
JavaScript
接口是 TypeScript 中用来定义对象结构的方式,它定义了对象的属性和方法的类型要求。
interface Person {
name: string;
age: number;
}
const person: Person = {
name: 'Alice',
age: 25
};
-- interface VS type
绝大多数情况下可以互换,用于定义对象结构,但也存在一些区别:
- 扩展性:interface 支持声明合并,多个同名接口会自动合并,在为第三方库扩展类型时很有用,type 不支持
- 实现:类可以 implements 一个 interface,但不能 implements 一个 type
- 复杂类型定义:type 的能力更强,可以用于定义联合类型、交叉类型、元组等任何复杂类型
- 总结
优先使用 interface 定义公共 API 的形状 (如对象、类),因为它具有更好的扩展性和面向对象的语义。
优先使用 type 定义复杂的、临时的类型,特别是联合类型、交叉类型或需要利用其他高级类型操作的场景。
泛型 (Generics)
JavaScript
在定义函数、类、接口等时,可以不预先指定具体类型,而是在使用时再指定具体类型,提升代码的复用性和灵活性
function log<T>(value: T): T {
return value;
}
let result = log(42); // T 被推断为 number
- <T> 是一个泛型参数,它允许该函数接收任意类型的参数,并且返回相同类型的值
- 其中 T 只是一个占位符,可以理解成 "类型变量"。函数调用时 T 将会被实际的类型所替代
索引类型 | 工具类型
索引类型:
JavaScript
索引类型允许根据给定的键来查询一个类型的值,或创建一个对象的类型
type Person = { name: string; age: number };
type PersonKeys = keyof Person; // "name" | "age"
let key: PersonKeys = "name";
let person: Person = { name: "John", age: 30 };
console.log(person[key]); // John
工具类型:
JavaScript
TypeScript 提供了一些内置的工具类型,帮助简化常见的类型操作
- Partial <T> : 将类型 T 的所有属性变为可选
interface Person {
name: string;
age: number;
}
type PartialPerson = Partial<Person>;
- Required <T> : 将类型 T 的所有属性变为必选
type RequiredPerson = Required<Person>;
- Readonly <T> : 将类型 T 的所有属性变为只读
type ReadonlyPerson = Readonly<Person>;
- Record <K, T> : 创建一个对象类型,其中键的类型为 K,值的类型为 T
type NameAge = Record<string, number>;
- Pick <T, K> : 从类型 T 中选取一部分属性 K
type NameOnly = Pick<Person, "name">;
- Omit <T, K> : 从类型 T 中省略掉属性 K
type AgeOnly = Omit<Person, "name">;
-- 注
不应孤立看待这些工具从而死记硬背,它们都是基于映射类型和条件类型构建的最佳实践封装
eg:Omit<T, K> = Pick + Exclude
条件类型 | 映射类型
JavaScript
- 条件类型根据给定的条件选择类型,语法为 T extends U ? X : Y
- 如果类型 T 可以赋值给类型 U(即 T extends U 为 true),则结果类型是 X,反之亦然
type IsString<T> = T extends string ? string : never;
type Test1 = IsString<string>; // string
type Test2 = IsString<number>; // never
1. 条件类型 + 推断(infer):
JavaScript
- infer R 是 TypeScript 的条件类型中的一种推断类型,用于从类型 T 中提取出函数的返回类型,并返回
- 以下代码主要基于 (... args : any []) => infer R 指定推断一个函数类型的返回值类型
type ReturnTypeOf<T> = T extends (...args: any[]) => infer R ? R : never;
type Test1 = ReturnTypeOf<() => string>; // string
type Test2 = ReturnTypeOf<() => number>; // number
type Test3 = ReturnTypeOf<() => boolean>; // boolean
type Test4 = ReturnTypeOf<string>; // never
2. 条件类型 + 映射类型:
JavaScript
- 以下代码主要基于 (infer U)[] 指定推断某个数组元素的类型
- 对于 泛型编程 或 不确定某类型是否是数组 时,此用法可以动态地提取其元素类型
type ElementType<T> = T extends (infer U)[] ? U : never;
type Test1 = ElementType<number[]>; // number
type Test2 = ElementType<string[]>; // string
type Test3 = ElementType<boolean[]>; // boolean
type Test4 = ElementType<number>; // never
3. 递归打平:
JavaScript
可以处理复杂的嵌套类型,以下代码通过递归展开嵌套数组,提取出它的最终元素类型,不管嵌套了多少层
type Flatten<T> = T extends (infer U)[] ? Flatten<U> : T;
type Test1 = Flatten<number[]>; // number
type Test2 = Flatten<string[][]>; // string
type Test3 = Flatten<(number | string)[]>; // number | string
4. 分配映射:
JavaScript
基于不同的传入类型条件映射到不同类型
type MyType<T> = T extends number ? string :
T extends boolean ? number :
T extends string ? boolean :
never;
type Test1 = MyType<number>; // string
type Test2 = MyType<boolean>; // number
type Test3 = MyType<string>; // boolean
type Test4 = MyType<undefined>; // never
5. 类型筛选、合并与转换
JavaScript
- 类型筛选:基于某些条件过滤出符合条件的类型,如筛选出某个类型的子集
- 类型合并:通过条件类型动态生成联合类型、交叉类型等复杂类型
- 类型转换工具:通过条件类型实现类型自动转换
以下代码可将对象的所有属性类型转换为 number:
type ToNumber<T> = {
[K in keyof T]: T[K] extends string ? number : T[K];
};
type Test = ToNumber<{ a: string; b: number }>;
// 结果:{ a: number; b: number }
JavaScript
映射类型用于将一个类型的所有属性映射到另一个类型,通常用在类型修改或创建新类型时
type Person = { name: string; age: number };
type ReadOnlyPerson = { readonly [K in keyof Person]: Person[K] };
let person: ReadOnlyPerson = { name: "John", age: 30 };
person.name = "Doe"; // 错误:不能修改只读属性
枚举 | 联合类型 | 交叉类型
JavaScript
- 枚举是一种基于数值(数字或字符串)的"集合类型",它允许你为一组值定义名字,可以是数字或字符串
- 枚举类型是一种对象类型,它将一组常量值封装成一个具名的集合(对象),包含了多个常量值的映射关系
- 枚举的类型定义是固定的,可以看作一个具名的集合体
**1. 数字枚举 **
JavaScript
- 数字枚举为一组数值提供名称映射。默认情况下,枚举的成员从 0 开始递增
- 可以手动设置枚举成员的初始值,其他成员将根据前一个成员的值递增。 enum Direction { Up, // 默认值是 0 Down, // 1 Left, // 2 Right // 3 } // 正向映射 let direction: Direction = Direction.Up; console.log(direction); // 输出: 0
// 反向映射
let directionName: string = Direction[1]; // 值为 1 的枚举成员是 'Down'
console.log(directionName); // 输出: Down
2. 字符串枚举
JavaScript
- 字符串枚举为每个枚举成员指定一个具体的字符串值,而不是自动递增的数字
- 通常用于需要更具可读性和可维护性的场景,如 HTTP 状态码、错误类型等 - 注意: 与数字枚举不同,字符串枚举不支持反向映射,因为每个值本身就是唯一的字符串
enum TaskStatus { NotStarted = "NOT_STARTED", InProgress = "IN_PROGRESS", Completed = "COMPLETED" } let task: TaskStatus = TaskStatus.InProgress; console.log(task); // 输出: IN_PROGRESS
3. 异构枚举
JavaScript
- 异构枚举是同时包含数字和字符串类型值的枚举,虽然不常见,但 TypeScript 允许
- 通常不推荐这样做,因为可能会导致代码理解上的混乱
enum MixedEnum {
No = 0,
Yes = "YES"
}
console.log(MixedEnum.No); // 输出: 0
console.log(MixedEnum.Yes); // 输出: YES
4. 常量枚举
JavaScript
- 一种特殊的枚举类型,编译时会被内联到代码中,避免了创建枚举对象的额外开销
- 它是为性能优化而设计的,只能包含常量成员
const enum Direction {
Up = 1,
Down,
Left,
Right
}
let direction = Direction.Up; // 这个会被直接编译为:let direction = 1;
5. 枚举的计算成员( Computed Members )
JavaScript
- 枚举成员不仅可以是常量值,还可以是通过计算得出的值
- TypeScript 允许你在枚举中使用表达式来计算成员的值
enum Direction {
Up = 1,
Down = Up * 2, // 计算得出:Up * 2 = 2
Left = "LEFT",
Right = "RIGHT"
}
console.log(Direction.Down); // 输出: 2
6. 使用场景
JavaScript
- 状态管理:比如表示任务、订单等状态的枚举
- 动作类型:比如表示用户操作或系统事件的枚举
- 配置选项:比如定义一组固定的选项来限制输入值
联合类型:
JavaScript
联合类型:表示一个值可以是几种不同类型之一,可以视作"类型放大"
let value: string | number;
value = 'Hello'; // valid
value = 42; // valid
优势:
打开编译选项strictNullChecks后,其他类型的变量不能赋值为 undefined或null
这时,如果某个变量确实可能包含空值,就可以采用联合类型的写法
let name:string|null;
交叉类型:
JavaScript
- 交叉类型:将多个类型合并成一个新类型,表示一个值同时具有多个类型的属性
- 它主要用于表示对象合成,也常用来为对象类型添加新属性
- 虽然用 类型别名(type)比较方便,但使用接口(interface)也可以实现
type Employee = { name: string };
type Worker = { job: string };
type EmployeeWorker = Employee & Worker;
const person: EmployeeWorker = {
name: 'Alice',
job: 'Developer'
};
[类型系统] 系统特性
类型别名 | 类型推断 | 类型断言
类型别名:
JavaScript
- 类型别名是给类型创建一个新名字,常用于简化复杂类型的声明
- 它与接口类似,但某些情况更适合定义 联合类型(union types)和 交叉类型(intersection types)
type Point = {
x: number;
y: number };
const p: Point = {
x: 10,
y: 20
};
一些特点:
1. 别名不允许重名,同一块级作用域中重复设置别名会报错
type Color = 'red';
type Color = 'blue'; // 报错
2. 别名具有块级作用域,不同块级作用域内的同名别名实际上并不相同
type Color = 'red';
if (Math.random() < 0.5) {
type Color = 'blue';
}
3. 别名允许嵌套,定义别名时可以使用其他别名
type World = "world";
type Greeting = `hello ${World}`;
类型推断
JavaScript
- TypeScript 能在 没有明确指定类型 或 设置类型注解 时 自动推断类型
- 它在大多数情况下可靠,可以减少类型注解冗余,提升开发效率
let num = 10; // TypeScript 自动推断 num 是 number 类型
类型断言
JavaScript
- 类型断言能覆盖 TypeScript 编译器的类型推断,通常在确信变量的类型时使用,可以使用 as 或 <> 语法
- 注意: 类型断言并不会进行类型检查,它只是让编译器假设值是某种类型,所以使用时要谨慎
let someValue: any = "hello";
let strLength: number = (someValue as string).length; // 将someValue断言为string
// 或者
let strLength2: number = (<string>someValue).length;
类型守卫 | 类型谓词
类型守卫:
JavaScript
- 类型守卫是用于在运行时确定一个变量的类型
- 常见的类型守卫有:
typeof:用来检查基本类型,如 string、number、boolean 等
instanceof:用来检查某个对象是否是某个类的实例
自定义类型守卫:通过函数来检查对象的类型
function isString(value: any): value is string {
return typeof value === 'string';
}
类型谓词:
JavaScript
- 上面代码中 value is string 是 TypeScript 的类型谓词(type predicate)语法
- 它的意思是:函数返回布尔值,若返回 true ,则 TypeScript 知道传入值 value 是 string 类型
声明文件(.d.ts)
javascript
TypeScript中可以通过声明文件(.d.ts)来为第三方库提供类型支持,尤其是在没有类型定义的情况下。声明文件用于描述库的结构。
declare module 'some-library' {
export function someFunction(): void;
}
[类与继承] 访问修饰符
markdown
- 在类与继承方面,TypeScript 相比 JavaScript 有两个主要特点:
- 静态类型检查
- 访问修饰符 ★
- TypeScript 访问修饰符:
- public:可以在任何地方访问
- private:只能在类内部访问
- protected:可以在类及其子类中访问
模拟访问修饰符:
JavaScript
- 而在 JavaScript 中不存在访问修饰符,类的属性和方法默认是 public 的
- 但可以通过一些技巧模拟类似的行为
1. 默认是 public
class Animal {
constructor(name) {
this.name = name; // 默认是 public
}
speak() {
console.log(`${this.name} makes a sound`);
}
}
2. 私有字段(ES2022引入)
class Animal {
#name; // 私有字段
constructor(name) {
this.#name = name;
}
speak() {
console.log(`${this.#name} makes a sound`);
}
3. 通过闭包模拟
function Animal(name) {
let _name = name; // 通过闭包定义私有变量
this.speak = function() {
console.log(`${_name} makes a sound`);
};
}
JavaScript
class Animal {
constructor(public name: string, private age: number) {}
speak(): void {
console.log(`${this.name} makes a sound`);
}
getAge(): number {
return this.age;
}
}
class Dog extends Animal {
constructor(name: string, age: number, public breed: string) {
super(name, age);
}
speak(): void {
console.log(`${this.name} barks`);
}
}
const dog = new Dog('Buddy', 3, 'Golden Retriever');
dog.speak();
console.log(dog.name); // 可以访问,因为 name 是 public
console.log(dog.age); // 编译时错误:age 是 private
[其他]
装饰器(Decorators)
markdown
- 装饰器是实验性功能,需要启用相应配置,它通常用于类的元编程中,允许你为类、方法、属性、参数等附加功能
- 在 JavaScript 中:
- 启用装饰器:需要安装并配置 Babel 插件 @babel/plugin-proposal-decorators
- JavaScript 装饰器功能基本上是运行时的操作,仍然不够稳定
- 在 TypeScript 中:
- 启用装饰器:tsconfig.json 设置 "compilerOptions": experimentalDecorators 为 true
- TypeScript 原生支持装饰器,语法更加成熟和稳定,并且使用时类型安全
类装饰器:用于类的构造函数
JavaScript
function logClass(target: Function) {
console.log(`Class ${target.name} is being created`);
}
@logClass
class Person {
constructor(public name: string) {}
}
// Class Person is being created
方法装饰器:用于类的方法
JavaScript
function logMethod(target: any, key: string, descriptor: PropertyDescriptor) {
console.log(`Method ${key} is being called`);
}
class Person {
@logMethod
greet() {
console.log('Hello!');
}
}
// Method greet is being called
// Hello!
- 详细解释:
- target:方法所在的类的原型对象(对于实例方法)或类构造函数(对于静态方法)
- key:方法名
- descriptor:PropertyDescriptor,用于描述方法的属性
- 对于 PropertyDescriptor,它是 JavaScript 中的一个接口,描述对象属性的特性
- 在装饰器中,PropertyDescriptor 用于描述方法装饰器的目标方法属性
interface PropertyDescriptor {
value?: any; // 属性的值。对于方法装饰器来说,通常表示方法本身
writable?: boolean; // 是否允许修改该属性
enumerable?: boolean; // 是否能在 for...in 或 Object.keys() 中被枚举出来
configurable?: boolean; // 是否可以删除或修改属性的特性
}
属性装饰器:用于类的属性
JavaScript
function logProperty(target: any, key: string) {
console.log(`Property ${key} is being accessed`);
}
class Person {
@logProperty
name: string;
}
// Property name is being accessed
参数装饰器:用于类方法的参数
JavaScript
function logParameter(target: any, key: string, index: number) {
console.log(`Parameter at index ${index} of method ${key} is being used`);
}
class Person {
greet(@logParameter name: string) {
console.log(`Hello, ${name}`);
}
}
// Parameter at index 0 of method greet is being used
// Hello, Alice
结语
不当之处恳请批评指正,感谢看到这里,觉得不错的话点个赞吧555
本人vx:wx072266,欢迎交流!