一、TS的一点概念
1.何为TS
TS作为一个JS的超集在原有的语法基础之上,添加了可选的静态类型和基于类的面向对象的编程
2.实际项目开发中
TS是面向解决大型且复杂的项目中的架构,以及代码维护复杂的场景;而JS是脚本化的语言,用于面向单一且简单的场景;而且,TS在编译期间可自主检测,主动发现并纠正错误,但是JS只能在运行时报错了
3.类型的检查以及运行的流程
TS和JS同为弱类型的语言,但TS却支持对于动态或者静态类型的检查,而JS没有静态类型的选项;TS是依赖于编译的也依赖于工程化体系的,而JS可直接在浏览器中运行。
4.简单的安装
** npm install -g typescript //全局安装 tsc -v //是否安装成功 tsc test.ts //运行 **
二、TS的基础类型和对应的写法
1. JS,TS
a、JS
let isEnabled = true; let class = 'JS'; let num=2; let a=null let b = undefined let arr=['js','css','html']
b.TS
let isEnabled: boolean = true; let class: string = 'Ts'; let num: number = 3; let a:null = null; let b: undefined = undefined let arr: Array<string> = ['js','css','html']或者let arr: string[] = ['js','css','html']
2.涉及元组tuple问题
let tuple:[string,number] tuple=['ts',88];
3.涉及枚举enum的问题
a.数字类枚举- 未赋值的情况下 默认是从数字零开始的 从左到右 或 从上到下,而且数字类枚举支持反向映射
enum Score{bad,notgood,good,perfect}; let sco: Score = Score.bad //值为0 //反向映射 let a: string = Score[0] //'bad'; let b: number = Score['bad'] //0
b.字符串类的枚举值 是不支持反向的映射的
enum stringNum{a='a',b='b'}; let c: string = stringNum.a //'a' let c: unknown = stringNum['a'] //报错
c.通常将混合类枚举值 视为 异构
enum Enum { A, // 0 B, // 1 C = 'C', D = 'D', E = 6, F, // 7 }
注意事项 若上边的Enum 中 E未作任何赋值 也就是`enum Enum {
A, // 0
B, // 1
C = 'C',
D = 'D',
E ,
F,
}'这个时候的编译器会提示报错:枚举成员必须具有初始化表达式,也即是 尽量 少做此类混合 若非得此操作 需要 明确每一个枚举成员的值
d.any类型- 绕过所有类型的检查 尽量避免使用any ,失效的筛查等于没有使用TS
e. unknown类型 - 可以绕过赋值的检查 但是禁止更改和传递
let a: unknown; a = true;或者 a = 'ss'; 或者 a = 88; let val1: unknown = a //可以 let val2: any = a //可以 ;但是 若 let val3: string = a //这是不可以的 ,因为 左边 确定的string而右边是 不确定的unknown
f.void类型 - 声明函数的返回值的(即是 当函数声明了 void 的返回类型 即此函数无 return)
function voidFunc(): void{console.log('ddd')}
若是这类写法 function voidFunc(): void{return 'eeeee'} //是报错的 因为声明了void 却又有return 则必报错
g.Object 在TS中类型化对象的创建
interface ObjectConstructor { create(o: object | null): any; //返回 any } const proto = {} Object.create(proto); Object.create(null); // Object.create(undefined); //error 会报错的,因为在使用接口时 我并未声明undefined
; 还例如 :interface Person { firstName: string; lastName: string; } const john: Person = { firstName: "John", lastName: "Doe" };
h.TS中直接给空对象是会报错的 原因在于:他违反了TS的静态类型系统。在TS中对象类型 默认是 “封闭的” ,故不可以直接添加 未声明的属性。但是你可以 调用 Object.prototype上的所有属性 因为 空对象 继承了Object.prototype上的所有属性
const obj = {} // obj.prop= 'hym' //error报错 // 但可以使用Object的所以方法 obj.toString(); //ok
二、接口 interface
tips:对行为的一种抽象,具体行为是由类实现(既可以描述对象的形状,又可以描述函数的形状)是不会转译为JS代码的,只在编译时起到一定的作用。
interface Class { name: string; time: number; } let hym: Class = { name: 'ts', time: 2 }
1.只读和任意的区分
只读:interface Class1 { readonly name: string; time: number; } // 面试题 - 只读和JS的引用是不同的 <=> const 的区分 let arr: number[] = [1, 2, 3, 4]; let ro: ReadonlyArray<number> = arr; //js里 只是栈地址不可更改 而堆信息是可改的 ,但是 只读 是两个都不可改 // ro[0] = 12 // ro.push(6) // ro.length = 100 // arr = ro//这些操作都是error
2.任意
任意:interface Class2 { readonly name: string; time: number; [proName: string]: any; //任意的(只确定name和time) } const c1 = { name: 'JS', time: 2 } const c2 = { name: 'BROWSER', time: 1 } const c3 = { name: 'ts', level: 2 }
三、交叉类型 & (它允许你将多个类型组合成一个新的类型。所得到的新类型将具有所有的特性。你可以将它看作是将多个类型“混合”在一起。)
interface Person { name: string; age: number; } interface Employee { employeeId: number; }
然后合并 type PersonEmployee = Person & Employee; const john: PersonEmployee = { name: "John", age: 30, employeeId: 12345 };
,也可以是 interface A {x: D} interface B {x: E} interface C {x: F} interface D {d: boolean} interface E {e: string} interface F {f: number} type ABC = A & B & C; let abc: ABC = { x: { d: false, e: 'zhaowa', f: 5 } }
tips、此外 会存在 合并冲突
例如:interface A { c: string, d: string } interface B { c: number, e: string } type AB = A & B;
此例中 不可能存在 c既满足string 又满足number 所以,这两个接口是不可以合并的
四、断言 类型声明、转换(断言好比其他语言中的类型转换,但不进行特殊的数据检查和结构调整。它没有运行时的影响,只是在编译阶段起作用)
1、尖括号形式声明
let valAny:any = 'hsjhf'; let valAnyNum: number =(valAny<string>).length
2、as 形式
let valAny:any = 'hsjhf'; let valAnyNum: number =(valAny as string).length
tips:当使用JSX语法的时候 只有 as 形式的断言有效 其余无效
3、非空断言 - 也就是 是 告诉编辑器 他的类型 可能还待定 但是 一定是有值的(不会为 null或者undefined)
type ClassTime = () => number const start = (classTime: ClassTime | undefined | null) => { let num = classTime!(); //具体的类型待定,但是非空(!)确认 }
;然后 还有就是 在 // let score: number; //无法保证 score 是非空的 let score!: number //保证 score 是非空的 也是 告知编辑器 在运行的时候会被赋值 startClass(); console.log(2 * score) function startClass() { score = 5 }
可以肯定一个值 必定在
五、类型守卫
定义两个接口:interface Teacher { name: string; courses: string[]; score: number; } interface Student { name: string; startTime: Date; score: string; } type class3 = Teacher | Student
;## 1.in -验证包含某种属性。function funCourse(cls:class3){if('courses' in cls){//teacher} if('stadrtTime' in cls){//student}}
;## 2.使用typeof - 来验证某种属性 function startScore(cls: Class3) { if (typeof cls.score === 'number') { // teacher } if (typeof cls.score === 'string') { // student } }
; ## 3.使用instanceof (这个例子 先转一下 接口为类class Teacher { name: string; courses: string[]; score: number; } class Student { name: string; startTime: Date; score: string; }
然后 function startCourse2(cls: Class3) { if (cls instanceof Teacher) { //teacher } if (cls instanceof Student) { //student } }
1.自定义类型---is
const isTeacher = function (cls: Teacher | Student): cls is Teacher { // 老师…… 为true 其余则是false return (cls as Teacher).courses !== undefined; }
六、TS进阶
1.函数的重载
首先列出所有的重载签名。然后提供一个实现签名(通常使用any
类型)。这个实现签名不是直接对外可见的,它是所有其他重载签名的兼容超集。最后是实际的函数实现,这个函数实现必须满足所有的重载签名。
function add(a: string, b: string): string; function add(a: number, b: number): number; function add(a: any, b: any): any { // 实际的函数实现 }
2.类方法的重载
与函数重载类似,首先列出所有的重载签名。但不同的是,你不需要提供一个明确的实现签名。实际的方法实现必须满足所有的重载签名。
type Combinable = string | number //Combinable是混合类型 class SomeClass { add(a: string, b: string): string; add(a: number, b: number): number; add(a: Combinable, b: Combinable): Combinable { // 实际的方法实现 } }
3.泛型 - 可重用的
创建可重用的组件,同时可以维护组件间的类型关系。就是 泛型可以允许你创建一个工作在多种数据类型上的函数或者类,而不是单一的数据类型。他有其一定的优势:1.是 保证了类型的一致性。2.是提高代码的复用率 减少冗余 比如:function identity(arg: any): any { return arg; }
arg 虽然可接受any 但是失去了arg的类型信息 ,所以我们 可以: function identity<T>(arg: T): T { return arg; }
这个T 就是传入的类型 并且在返回值中再次使用他的类型 ,当然 很多时候 我们无需 传入这个 类型 因为 TS可以自动推断我们所传的类型 而且也可简化代码 所以 就可如此调用 let a = identity('3'); 当然 也需要 去指定特定的类型 如function combine<T>(input1: T, input2: T): T[] { return [input1, input2]; } const result = combine(3, 'hello'); // 错误,因为3和'hello'类型不匹配 const validResult = combine<string | number>(3, 'hello'); // 正确,因为我们明确指定了泛型类型
一切由实际的开发而定
4.装饰器- decorators
主要是用于: 1.AOP(面向切面编程)如,记录、度量、数据检查 2.与框架有关的,如angular里 使用装饰器来定义组件和服务,当然在 react的生态的一些工具或者库有用到 如,MobX。简单的例子:function log(target: any, propertyKey: string, descriptor: PropertyDescriptor): void { let originalMethod = descriptor.value; descriptor.value = function(...args: any[]) { console.log(""+Calling method: ${propertyKey} with args: ${JSON.stringify(args)+""}); return originalMethod.apply(this, args); } }class MyClass { @log add(a: number, b: number): number { return a + b; } }const obj = new MyClass(); obj.add(1, 2); // 输出: Calling method: add with args: [1,2]
**您给出了两种不同的装饰器:类装饰器和属性装饰器。我会为您详细解释这两种装饰器的工作方式。
- 类装饰器 (
Zhaowa
)
此装饰器作用于 Course
类。当您使用 @Zhaowa
装饰一个类时,该装饰器函数会被调用,并将类的构造函数作为其唯一参数。在此装饰器内,您给类的原型添加了一个 startClass
方法:
function Zhaowa(target: Function): void {
target.prototype.startClass = function(): void {
// 逻辑
}
}
因此,使用了 @Zhaowa
装饰器的 Course
类现在拥有了一个 startClass
方法。
- 属性装饰器 (
nameWrapper
)
此装饰器作用于类的属性。当您使用 @nameWrapper
装饰一个属性时,装饰器函数会被调用,并将两个参数传递给它:第一个是类的原型或构造函数(静态属性),第二个是属性名:
function nameWrapper(target: any, key: string): void {
Object.defineProperty(target, key, {})
}
在您的 nameWrapper
装饰器中,您使用了 Object.defineProperty
来定义或修改一个属性。但是,您给出的定义是空的,这会导致 name
属性变为不可配置、不可枚举和不可写。如果您的目的是这样的,那么这是有效的。否则,您可能需要为 Object.defineProperty
提供一些属性描述符。
现在,您可以这样使用这两种装饰器:
@Zhaowa
class Course {
constructor() {
// 业务逻辑
}
@nameWrapper
public name: string;
}
const myCourse = new Course();
myCourse.startClass(); // 使用由Zhaowa装饰器添加的方法
console.log(myCoursea.name) //使用nameWrapper 赋予的name属性 但值为undefined
装饰器为您提供了一种优雅的方式来修改或增强类和类的属性、方法或参数。但请注意,由于装饰器是实验性的特性,使用它们时要确保您了解其工作原理和潜在的陷阱。**
七、简单的归结下 TS的一个编译流程
**假设我们有以下TypeScript源码:
let num: number = 10;
-
源码 (Source Code):
let num: number = 10;
-
Scanner (扫描器): 生成的令牌流可能是:
[ 'let': 'keyword', 'num': 'identifier', ':': 'punctuation', 'number': 'type', '=': 'assignment', '10': 'integer', ';': 'eos' ]
-
Parser (解析器): 生成的抽象语法树 (AST) 可能如下:
{ kind: 'VariableStatement', declarationList: { kind: 'VariableDeclarationList', declarations: [{ kind: 'VariableDeclaration', name: { kind: 'Identifier', text: 'num' }, type: { kind: 'TypeReference', typeName: { kind: 'Identifier', text: 'number' } }, initializer: { kind: 'NumericLiteral', value: 10 } }] } }
-
Binder (绑定器):
- 创建符号表,可能会为变量
num
创建一个符号。 - 符号与AST中的
num
节点相关联。
- 创建符号表,可能会为变量
-
Checker (校验器):
- 语法检查:确保语法是正确的(在这种情况下,它是)。
- 类型检查:确保
num
的赋值与其声明的类型匹配。在这里,num
的类型是number
,并且赋值给它的值是一个数字,所以一切正常。
-
Emitter (发射器): 生成的JavaScript代码为:
let num = 10;
注意:第二和第三步比较难以理解 分解理解 :
第2步: Scanner (扫描器) 扫描器的任务是读取源代码并将其拆分成一个个的“令牌”。每个令牌都是代码中的最小单位,比如关键字、标识符、数字、运算符等。
举个例子,对于代码let num: number = 10;
,扫描器会将它拆分成这样的令牌流:
[
'let': 'keyword',
'num': 'identifier',
':': 'punctuation',
'number': 'type',
'=': 'assignment',
'10': 'integer',
';': 'eos'
]
这就像是将句子拆分成单词,使我们能够对其进行更深入的分析。
第3步: Parser (解析器) 解析器的任务是取这些令牌,并构建一个称为“抽象语法树” (AST) 的结构,它表示代码的层次结构和语义。
基于上面的令牌,解析器可能会创建如下的AST:
{
kind: 'VariableStatement',
declarationList: {
kind: 'VariableDeclarationList',
declarations: [{
kind: 'VariableDeclaration',
name: { kind: 'Identifier', text: 'num' },
type: { kind: 'TypeReference', typeName: { kind: 'Identifier', text: 'number' } },
initializer: { kind: 'NumericLiteral', value: 10 }
}]
}
}
这个结构可以这样理解:
- 我们有一个变量声明语句(
VariableStatement
)。 - 该声明语句包含一个变量声明列表(
VariableDeclarationList
)。 - 列表中有一个变量声明(
VariableDeclaration
)。 - 该变量声明有一个名为
num
的名称(Identifier
),一个类型为number
的类型引用(TypeReference
)和一个初始值为10的数字字面量(NumericLiteral
)。
这个AST为我们提供了一种方式,可以理解和操作代码的结构,而无需关心代码的具体文本格式。
简而言之,扫描器将源代码分解成一个个令牌,然后解析器将这些令牌组合成一个代表代码结构的AST**