基础概念
TS 是在 JavaScript 语法的基础上进行扩展和增强的超集,尤其在类型系统方面,为开发者提供了更严格和明确的类型定义和约束。
面向项目
- TS 主要面向解决大型复杂项目,其架构和代码维护较为复杂。例如大型企业级应用、多人协作开发的项目等。
- JS 是脚本化语言,常用于面向简单页面场景。
自主检测
- TS 在编译时能主动发现并纠正错误。
- JS 往往在运行时才报错。
类型检测
- TS 是强类型语言,支持类型检测,并且可以在编译时进行类型检查和提示。
- JS 是弱类型语言,无静态类型选项。
运行流程
- TS 依赖编译,编译后的产物(通常是 JavaScript 代码)最终在浏览器中运行。
- JS 可直接在浏览器中运行。
检测阶段
- 在 TypeScript 中,大部分的类型检测和纠错是在编译阶段进行的。
- 但是在一些特定的运行时环境(如某些 Node.js 应用)中,也可能存在一些运行时的类型检查机制或工具。
安装运行
安装
显示版本就安装成功了,也可以选择项目局部安装
npm install -g typescript
tsc -v
运行编译
tsc demo.ts
TS基础类型和写法
常用数据
boolean | string | number | array | null | undefined | Array | ReadonlyArray |object
//js
let isEnabled = true
let course = 'haha'
let classNum = 2
let u = undefined
let n = null
let classArr = ['basic', 'execute']
//ts
let isEnabled: boolean = true
let course: string = 'haha'
let classNum: number = 2
let u: undefined = undefined
let n: null = null
//数组
//let classArr: string[] = ['basic', 'execute']
let classArr: Array<string> = ['basic', 'execute']
let arr: number[] = [1, 3, 4, 5, 6]
let ro: ReadonlyArray<number> = arr
arr.push(7) // OK
ro[0] = 12 // 赋值 - Error
ro.push(7) // 增加 - Error
ro.length = 100 // 长度改写 - Error
object 类型用于表示非原始类型的值,即不是 number 、 string 、 boolean 、 null 或 undefined 的类型。
let obj: object = { name: 'John' };
let arr: object = [1, 2, 3];
let func: object = function() {};
object 类型相对比较宽泛,不能准确指定对象的具体属性和结构。
如果您想要更精确地描述对象的结构和属性,可以使用接口(interface )或类型别名(type )来定义特定的对象类型。
它包括对象、数组、函数以及其他更复杂的数据结构。
interface Person {
name: string;
age: number;
}
let person: Person = { name: 'John', age: 30 };
tuple - 元组
let tupleType: [string, boolean];
tupleType = ['haha', true];
enum - 枚举
以下例子的ScoreRes变量,均为demo.ts执行tsc编译后,Score的值
默认从0开始枚举
enum Score {
A,
B,
C,
D
}
let score: Score = Score.A
console.log(score); //0
console.log(Score[0]);//A
console.log(Score.A);//0
const ScoreRes =
{
'0': 'A',
'1': 'B',
'2': 'C',
'3': 'D',
A: 0,
B: 1,
C: 2,
D: 3
}
定义数值后,会往后依次递增
//enum Score {
// A=-2,
// B,
// C,
// D='eee',//这里会导致后面报错,要么连着E,F也一起定义值
// E,
// F
// }
enum Score {
A=-2,
B,
C,
D=7,
E,
F
}
let score: Score = Score.A
console.log(score); //-2
console.log(Score[0]);//C
console.log(Score[8]);//E
console.log(Score.C);//0
const ScoreRes =
{
'-2': 'A',
'-1': 'B',
'0': 'C',
'7': 'D',
'8': 'E',
'9': 'F',
A: -2,
B: -1,
C: 0,
D: 7,
E: 8,
F: 9
}
没有按照从小到大定义数值的情况
- 会导致枚举异常,丢失value对key的索引,建议还是从小到大定义枚举值
enum Score {
A=2,
B,
C,
D=1,
E,
F
}
const ScoreRes =
{
'1': 'D',
'2': 'E',
'3': 'F',
'4': 'C',
A: 2,
B: 3,
C: 4,
D: 1,
E: 2,
F: 3
}
几个特殊类型
any | unknown | void | never
any
any 类型是一种非常灵活但也需要谨慎使用的类型。
any 类型允许您对变量进行任何操作,忽略类型检查。这意味着您可以将任何值赋给 any 类型的变量,并且可以对其执行任何操作,而 TypeScript 编译器不会给出类型错误提示
//绕过所有的类型检查
let anyValue: any = 123
anyValue = 'anyValue'
anyValue = false
anyValue = new Error()
unknown
unknown 类型是一种安全的、类型未知的类型。
与 any 类型不同,unknown 类型更加安全和严谨。当一个变量被声明为 unknown 类型时,您不能对其进行直接的操作,除非先进行类型断言或类型缩小。
- unknown 类型表示的是未知的类型,不能直接将其赋值给(value1)特定的类型,如 boolean 类型。
- 将一个值声明为 unknown 类型时,TypeScript 编译器不知道这个值的确切类型和结构。所以,直接将其赋值给明确的类型(如 boolean )是不被允许的,因为无法确定这个 unknown 值是否真的能兼容 boolean 类型。
let unknownValue: unknown
unknownValue = 123
unknownValue = 'unknownValue'
unknownValue = true
let value: unknown;
if (typeof value ==='string') {
value.toUpperCase(); // 此时可以操作,因为经过类型判断确定为字符串类型
}
let value1: boolean = unknownValue // NOK
let value2: any = unknownValue // OK
let value3: unknown = unknownValue // OK
void
在 TypeScript 中,void 类型用于表示函数没有返回值的情况。
- 例如,如果一个函数只是执行一些操作而不返回任何有意义的值,它的返回类型可以被指定为
void:
function printMessage(message: string): void {
console.log(message);
}
never
never 类型表示那些永远不会有返回值的函数的返回类型。
- 函数中存在无限循环或者抛出异常,导致函数永远无法正常结束
function infiniteLoop(): never {
while (true) {}
}
- 函数的分支涵盖了所有可能的情况,并且这些分支最终都会抛出错误或者进入无限循环。
function error(message: string): never {
throw new Error(message);
}
定义类型
interface(接口)
- 主要用于定义对象的形状和行为。
- 支持声明合并,即可以多次声明同一个接口,新的声明会与之前的声明合并。
- 更适合描述具有公共结构的对象类型。
interface Person {
readonly name: string; //只读
age?: number; //可选
}
interface Person {
occupation: string;
[propName: number]: number;//
}
let person: Person = {
name: 'John',
//age: 30,//可有可无
occupation: 'Developer',
1:1//合法
};
propName 在上面只是一个占位符,表示具有数字类型索引(键)的属性。
这意味着可以为这个 Class 类型的对象添加任意数量的以数字为键的属性,并且这些属性的值可以是任何类型(由 any 指定)。
extends-继承
extends 关键字用于类的继承。通过继承,可以在子类中复用父类的属性和方法,并可以添加新的属性和方法或者重写父类的方法。
interface Class {
name: string
}
interface Course1 {
}
interface Course2 extends Class {
score: number
}
- 也有说可以用于类型缩小的说法; 例如,假设有一个父类
BaseClass和一个子类DerivedClass继承自BaseClass:
class BaseClass {
baseProperty: string;
}
class DerivedClass extends BaseClass {
derivedProperty: number;
}
function process(obj: BaseClass) {
if (obj instanceof DerivedClass) {
// 在这里,由于通过 instanceof 进行了类型判断,实现了类型缩小
// 可以安全地访问 DerivedClass 特有的属性 derivedProperty
console.log((obj as DerivedClass).derivedProperty);
}
}
在上述示例中,通过 instanceof 操作符进行类型判断,如果对象是 DerivedClass 的实例,就实现了类型的缩小,从而可以在后续代码中访问子类特有的属性或方法。
type(类型别名)
- 可以用于定义各种类型,不仅仅是对象类型,还包括联合类型、交叉类型、元组等更复杂的类型。
- 不支持声明合并,重复定义会报错。
type Person = {
name: string;
age: number;
};
type MyUnion = string | number;
重复定义
type Class = {
name: string
}
type Class = {//会报错,标识符“Class”重复。
score: number
}
交叉类型"&" + 联合类型"|"
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: 'ts',
//f: 5 //可选
}
}
type Class = {
name: string
}
type Course = {
nickName: string
}
type NameObj = Class | Course
let nameObj:NameObj={name:'小明'}//{ nickName: '阿明' }
type DetailObj = Class & Course
let detailObj:DetailObj={name:'小明', nickName: '阿明'}
type TupleClass = [Class, Course]
let arr:TupleClass=[{name:'小明'}, { nickName: '阿明' }]
灵活性
type Keys = 'name' | 'nickName'
type Class = {
[key in Keys]: string
}
const haha: Class = {
name: 'typescript',
nickName: 'ts'
}
type和interface的区别
相同点
- 都用于定义类型:它们的主要目的都是为了给数据结构或类型提供明确的定义,以增强代码的类型安全性和可读性。
- 可以描述对象结构:两者都能够描述对象应该具有的属性及其类型。
- 可用于函数类型定义:都可以用来定义函数的参数和返回值的类型。
- 作为类型约束:在函数参数、变量声明等地方,可以作为类型约束来确保数据的类型符合预期。
- 提升代码的可维护性和可理解性:通过清晰地定义类型,使代码更易于理解和维护,减少类型相关的错误。
不同点
- 定义对象类型的方式:
interface主要用于定义对象的形状和结构,而type不仅可以定义对象类型,还能定义联合类型、交叉类型、元组等更复杂的类型。 - 声明合并:
interface支持声明合并,即可以多次声明同一个接口,新的声明会与之前的声明合并。而type不支持声明合并。 - 灵活性:
type更加灵活,能处理更广泛和复杂的类型定义情况。 - 应用场景:
interface更适合描述具有公共结构的对象类型,type则在需要定义复杂或特殊类型结构时更具优势。
总的来说,interface 侧重于对象类型的定义和扩展,type 则在类型定义的多样性和灵活性方面表现出色,选择使用哪种方式取决于具体的需求和项目的代码风格。
类型不兼容/类型冲突
interface A {
c: string;
d: string;
}
interface B {
c: number;
e: string;
}
//合并c的关系 => c: never
type AB = A & B //错误写法c的值不可能是string的同时也是number
type AA = {
c: string;
d: string;
}
type BB = {
c: number;
e: string;
}
type AABB =AA & BB
const aabb:AABB = {d:'d',e:'e',c:"1"} // 不能将类型“string”分配给类型“never”。
断言
类型断言的两种形式
- “尖括号”语法:
<类型>值 as语法:值 as 类型
// 尖括号声明 阶段性的类型
let anyValue: any = 'haha'
let anyLength: number = (<string>anyValue).length
// as声明
let anyValue: any = 'haha'
let anyLength: number = (anyValue as string).length
// 非空声明
type ClassTIme = () => number
const start = (classTime: ClassTime | undefined) => {
let num = classTime!() // 具体类型可能为多种,但是非空确认
}
主要用途包括
- 当编译器无法自行推断出某个变量的更具体类型时,开发者明确告诉编译器以特定类型来处理该值。
-
如果从某个复杂的操作中获取了一个值,但其类型不够精确,通过类型断言可以让后续对该值的操作基于更准确的类型。
- 在某些情况下,虽然编译器可能知道某个值的类型,但由于类型系统的限制,需要通过断言来访问特定类型的属性或方法。
需要注意的是,类型断言不会执行任何类型转换或检查,只是告诉编译器以指定的类型来处理值。如果断言不正确,在运行时可能会导致错误。
类型守卫
-
是用于在运行时确定变量类型的一种机制。
-
类型守卫的主要作用是在特定的条件判断中,让 TypeScript 能够更精确地了解变量的类型,从而允许进行更准确的类型检查和智能提示。
常见的类型守卫方式
typeof、instanceof、函数自定义
typeof类型守卫:通过typeof操作符来判断基本类型(如string、number、boolean等)。
function processValue(value: string | number) {
if (typeof value ==='string') {
// 在这里,TypeScript 知道 value 是字符串类型
console.log(value.toUpperCase());
} else {
// 在这里,TypeScript 知道 value 是数字类型
console.log(value + 10);
}
}
instanceof类型守卫:用于判断一个对象是否是某个类的实例。
class Parent {}
class Child extends Parent {}
function process(obj: Parent) {
if (obj instanceof Child) {
// 在这里,TypeScript 知道 obj 是 Child 类型
console.log('It is a Child instance');
} else {
// 在这里,TypeScript 知道 obj 是 Parent 类型
console.log('It is a Parent instance');
}
}
- 自定义类型守卫:可以通过函数来实现自定义的类型守卫逻辑。
interface Bird {
fly(): void;
}
interface Dog {
bark(): void;
}
function isBird(animal: Bird | Dog): animal is Bird {
return (animal as Bird).fly!== undefined;
}
function processAnimal(animal: Bird | Dog) {
if (isBird(animal)) {
animal.fly();
} else {
animal.bark();
}
}
泛型
泛型是一种使函数、类、接口等能够在定义时不指定具体的数据类型,而在使用时再确定类型的机制。
class GenericClass<T> {
value: T;
constructor(value: T) {
this.value = value;
}
}
interface GenericInterface<T> {
data: T;
}
function startClass<T, U>(name: T, score: U): T {
return name + score;
}
function startClass<T, U>(name: T, score: U): String {
return name + score;
}
function startClass<T, U>(name: T, score: U): T {
return (name + String(score)) as T
}
function startClass<T, U>(name: T, score: U): T {
return (name + String(score)) as T
}
startClass<string, number>('ts', 5)
装饰器
装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。 装饰器使用 @expression 这种形式,expression 求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。
- 使用
tsc demo.ts编译可能会报错,执行以下热更新脚本可以看到编译后的jstsc fileName.ts --target ES5 -w --experimentalDecorators
- 装饰器的执行顺序是从下往上,然后从右往左的
以是否能携带参数作为区分,可以分为两类装饰器,无参数的装饰器和带参数的装饰器
无参数的装饰器
- 是一个函数,它接收被装饰的目标(比如类、方法、属性等)作为参数,并可以对其进行修改或增强。
function logClass(target: Function) {
console.log(`Decorating class: ${target.name}`);
}
@logClass
class MyClass {}
带参数的装饰器
- 带参数的装饰器是一个返回装饰器函数的函数
function logClassWithParam(message: string) {
return function(target: Function) {
console.log(`Decorating class with message: ${message}`);
};
}
@logClassWithParam('This is a custom message')
class MyClass {}
以作用的目标作为区分,可以分为五类装饰器
类装饰器(Class Decorators)属性装饰器(Property Decorators)方法装饰器(Method Decorators)参数装饰器(Parameter Decorators)访问器装饰器(Accessor Decorators)
类装饰器
应用于类的定义。它可以修改类的构造函数,或者为类添加一些静态属性和方法。
一个参数
类装饰器的参数是类的构造函数。
返回值
- 类装饰器函数的返回值可以用来修改或替换被装饰的类。
- 类装饰器函数不返回值(或者返回
undefined),那么被装饰的类将保持其原始定义不变(返回null会报错)。
如向原型添加方法或属性,都只是对原始类的补充,而不会改变类的整体结构和定义。
以下例子是操作类和类的原型对象,以及实现单例模型的使用场景
function sealed(constructor: Function) {
// 在这里可以对类的构造函数进行操作
Object.seal(constructor);//对MyClass设置为不可扩展
Object.seal(constructor.prototype);//将MyClass的原型对象设置为不可扩展
}
let instance: any;
//单例模式
function Singleton<T extends { new (...args: any[]): {} }>(constructor: T) {
if (!instance) {
instance = new constructor();
}
return instance;
}
//@sealed
@Singleton
class MySingletonClass {}
const instance1 = new MySingletonClass();
const instance2 = new MySingletonClass();
console.log(instance1 === instance2);
属性装饰器
应用于类的属性。可以用于监视、修改或验证属性的行为。
两个参数
- 目标对象:
- 如果在
实例属性上,目标对象就是类的原型; - 如果在
静态属性上,目标对象就是类本身。
- 如果在
- 属性名
以下例子包括实际使用场景和验证目标对象
let ppt,sPpt;
function readonly(target: Object, propertyKey: string) {
ppt=target;
Object.defineProperty(target, propertyKey, {
writable: false,//不可写入
value:'jaja'
});
}
function staticProp(target: Object,propertyKey: string) {
sPpt=target;
}
class MyClass {
@readonly
public property?: string;
@staticProp
static sProperty?:string
}
console.log('ppt',MyClass.prototype===ppt);//true,类的原型
console.log('sPpt',MyClass===sPpt);//true,类本身
const myClass=new MyClass();
console.log(myClass.property);//jaja
myClass.property='haha'
console.log(myClass.property);//jaja,不可写入
方法装饰器
应用于类的方法。可以修改方法的行为,例如添加日志、性能测量等功能。
方法装饰器有三个参数:
- 目标对象:如果方法是在实例上,目标对象就是类的原型;如果方法是静态的,目标对象就是类本身。
- 方法名
- 属性描述符
属性描述符
value:属性的值(这里是方法本身)。writable:决定属性值是否可写。如果为false,则不能重新赋值给该属性。enumerable:决定属性是否可枚举。如果为false,在使用for...in循环或Object.keys()等方法时,该属性将不会被包含。configurable:决定属性是否可配置。如果为false,则不能删除该属性,也不能修改其writable、enumerable和configurable特性。 以下例子实现了一个增加函数调用日志的修饰器函数
function logMethod(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`调用了函数: ${propertyKey}`);
return originalMethod.apply(this, args);
};
}
class MyClass {
@logMethod
public method(msg:string) {
console.log('msg',msg);// 方法的实现
}
}
new MyClass().method('haha') // 调用了函数: method //msg haha
参数装饰器
应用于方法的参数。可以用于参数的验证、转换等操作。
无法直接访问到参数的值
参数装饰器有三个参数:
- 目标对象:如果方法是在实例上,目标对象就是类的原型;如果方法是静态的,目标对象就是类本身。
- 方法名
- 参数在参数列表中的索引
function markParameter1(param: string): ParameterDecorator {
return function(target: any,propertyKey: string | symbol,parameterIndex: number):void {
console.log('markParameter1-param',param); // 哈哈
console.log('markParameter1-target',target); // MyClass { method: [Function (anonymous)] }
console.log('markParameter1-propertyKey',propertyKey); // method
console.log('markParameter1-parameterIndex',parameterIndex); // 0
}
}
function markParameter2(target: any,propertyKey: string | symbol,parameterIndex: number):void {
console.log('markParameter2-target',target); // MyClass { method: [Function (anonymous)] }
console.log('markParameter2-propertyKey',propertyKey); // method
console.log('markParameter2-parameterIndex',parameterIndex); // 1
}
class MyClass {
//从右往左执行,先调用markParameter2,后调用markParameter1
method(@markParameter1('哈哈') param1: number, @markParameter2 param2: string) {
// 方法的实现
}
}
const myClass=new MyClass();
访问器装饰器
类装饰器
应用于类的访问器(getter 和 setter)。
访问器装饰器有三个参数,与方法装饰器相同:
- 目标对象:如果访问器是在实例上,目标对象就是类的原型;如果访问器是静态的,目标对象就是类本身。
- 访问器名
- 属性描述符
使用场景:数据验证和过滤,日志记录,权限控制,数据同步和更新
以下例子为数据限制
function rangeValidator(min: number, max: number) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalSetter = descriptor.set;
descriptor.set = function (value: number) {
if (value < min) {
value = min;
} else if (value > max) {
value = max;
}
originalSetter.call(this, value);
};
const originalGetter = descriptor.get;
descriptor.get = function () {
const value = originalGetter.call(this);
return value;
};
};
}
class Temperature {
private _temperature: number;
@rangeValidator(0, 100)
get temperature() {
return this._temperature;
}
@rangeValidator(0, 100)
set temperature(newValue: number) {
this._temperature = newValue;
}
}
const temp = new Temperature();
temp.temperature = -10;
console.log(temp.temperature); // 0
temp.temperature = 150;
console.log(temp.temperature); // 100
日志记录可以在getter,setter内增加log;权限控制可以在getter,setter内对值做鉴权,非法值可以抛出错误等;
参考文档:[ts.nodejs.cn/docs/handbo…]
其他(元数据): 关于元数据直接使用tsc编译,获取以下内置值时会异常的问题;
design:returntype,design:type,design:paramtypes
如果没有启用 emitDecoratorMetadata 选项,编译器不会生成这些元数据,Reflect.getMetadata('design:returntype', People.prototype, 'xxx') 会返回 undefined;
执行以下脚本开启emitDecoratorMetadata
tsc --experimentalDecorators --emitDecoratorMetadata