TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale.
TypeScript 简介
什么是 TypeScript
TypeScript 是 JavaScript 的超集,TS 相比 JS 的优势:
- 更早发现错误,减少找/改 BUG 时间,提升开发效率
- 程序中任何位置的代码都有代码提示,随时随地的安全感,增强开发体验
- 强大的类型系统提升了代码的可维护性,使得重构代码更容易
- 支持最新的 ECMAScript 语法,优先体验最新的语法
- TS 类型推断机制,不需要在代码中的每个地方都显示标注类型
此外,Vue3 源码使用 TS 重写,Angular 默认支持 TS,React 与 TS 完美配合,TypeScript 已成为大中型前端项目的首选语言。
安装 TypeScript
由于 Node.js/浏览器只认识 JS 代码,所以需要将 TS 代码转换为 JS 代码,才能运行。
1 2
| $ npm install -g typescript
|
以上命令会在全局环境下安装 tsc 命令,安装完成之后,我们就可以在任何地方执行 tsc 命令了
编译并运行 TypeScript
- 创建
hello.ts 文件
1 2 3 4 5 6
| function sayHello(person: string) { return 'Hello, ' + person; }
let user = 'Zhang'; console.log(sayHello(user));
|
- 将 TS 编译为 JS
- 执行 JS 代码
简化运行 TS
使用 ts-node 可以直接运行 TS 代码,不需要编译为 JS 代码
1
| $ npm install -g ts-node
|
使用:
TypeScript 基础
原始数据类型
JavaScript 的类型
- 原始数据类型(Primitive data types):
Boolean,Null,Undefined,Number,String,Symbol,以及 ES10 中的新类型 BigInt
- 原始数据类型(Primitive data types):
Object
TypeScript 新增类型
联合类型,自定义类型(类型别名),接口,元组,字面量类型,枚举,void,any 等
1 2 3 4 5 6 7 8 9
| let isDone: boolean = false; let decLiteral: number = 6; let myName: string = 'Tom'; function alertName(): void { alert('My name is Tom'); } let u: undefined = undefined; let n: null = null; let s: symbol = Symbol();
|
数组类型
数组类型有 2 种写法:「类型 + 方括号」表示法和数组泛型(Array Generic)
1 2
| let fibonacci: number[] = [1, 1, 2, 3, 5]; let fibonacci: Array<number> = [1, 1, 2, 3, 5];
|
接口也可以用来描述数组:
1 2 3 4
| interface NumberArray { [index: number]: number; } let fibonacci: NumberArray = [1, 1, 2, 3, 5];
|
NumberArray 表示:只要索引的类型是数字时,那么值的类型必须是数字
联合类型
1 2 3 4
| let arr: (string | number)[] = [1, '2', 3]; let arr1: number | string[] = ['a', 'b', 'c']; let arr2: number | string[] = 123;
|
类型别名
类型别名(自定义类型):为任意类型起别名
使用场景:当复杂的同一类型被多次使用时,可以通过类型别名,简化该类型的使用
1 2 3
| type CustomArray = (number | string)[] let arr1: CustomArray = [1, '2', 3]; let arr2: CustomArray = ['a', 'b', 6, 7];
|
函数类型
在 JavaScript 中,有两种常见的定义函数的方式——函数声明(Function Declaration)和函数表达式(Function Expression):
1 2 3 4 5 6 7 8 9
| function sum(x, y) { return x + y; }
let mySum = function (x, y) { return x + y; };
|
TypeScript 为函数指定类型的两种方式:
- 单独指定参数,返回值类型
1 2 3
| function add(num1: number, num2: number): number { return num1 + num2 }
|
1 2 3 4
| const add = (num1: number, num2: number): number => { return num1 + num2 }
|
- 同时指定参数,返回值类型
1 2 3
| const add: (num1: number, num2: number) => number = (num1, num2) => { return num1 + num2 }
|
当函数作为表达式时,可以通过类似箭头函数形式的语法来为函数添加类型
这种形式只适用于函数表达式
可选参数
使用函数实现某个功能时,参数可以传也可以不传。这种情况下,在给函数参数指定类型时,需要使用可选参数
1 2 3 4 5 6 7 8 9
| function buildName(firstName: string, lastName?: string) { if (lastName) { return firstName + ' ' + lastName; } else { return firstName; } } let tomcat = buildName('Tom', 'Cat'); let tom = buildName('Tom');
|
可选参数只能出现来参数列表的最后,也就是说可选参数后面不允许再出现必需参数了
对象类型
JS 中的对象是由属性和方法构成的,而 TS 中对象的类型就是在描述对象的结构(有什么类型的属性和方法)
1 2 3 4 5 6 7
| let person: { name: string; age: number; sayHi(): void } = { name: 'Zhang', age: 20, sayHi() { console.log('hi') } }
|
- 直接使用
{} 来描述对象结构,属性采用属性名:类型的形式;方法采用方法名():返回值类型的形式
- 在一行代码中指定对象的多个属性类型时,使用
;来分隔
- 如果一行代码只指定一个属性类型(通过换行来分隔多个属性类型),可以去掉
;
- 方法的类型也可以使用箭头函数形式(
{sayHi: () => void})
1 2 3 4 5 6 7 8 9 10 11
| let person: { name: string age: number sayHi: () => void } = { name: 'Zhang', age: 20, sayHi() { console.log('hi') } }
|
可选属性
对象的属性或方法也可以是可选的,此时需要用到可选属性
1 2 3 4
| function myAxios(config: { url: string; method?: string }) { console.log(config) }
|
接口
当一个对象类型被多次使用时,一般会使用接口来描述对象的类型,达到复用的目的。解释:
- 使用
interface 关键字来声明接口
- 接口名称可以是任意合法的变量名称
- 声明接口后,直接使用接口名称作为变量的类型
- 因为每一行只有一个属性类型,因此属性类型后面没有
;
1 2 3 4 5 6 7 8 9 10 11 12 13
| interface IPerson { name: string age: number sayHi(): void }
let person: IPerson = { name: 'Zhang', age: 20, sayHi() { console.log('hi') } }
|
interface 和 type 的对比
- 相同点:都可以给对象指定类型
- 不同的:
- 接口 interface,只能为对象指定类型
- 类型别名 type,不仅可以为对象指定类型,实际上可以为任意类型指定别名
1 2 3 4 5 6 7 8 9 10 11 12 13
| interface IPerson { name: string age: number sayHi(): void }
type IPerson = { name: string age: number sayHi(): void }
type NumStr = number | string
|
接口继承
如果两个接口之间有相同的属性或方法,可以将公共的属性或方法抽离出来,通过继承来实现复用
1 2
| interface Point2D { x: number; y: number } interface Point3D { x: number; y: number; z: number }
|
更好的方式:
1 2
| interface Point2D { x: number; y: number } interface Point3D extends Point2D { z: number }
|
- 使用
extends 关键字实现了接口继承
- 继承后拥有了父接口的所有属性和方法
元组类型
使用场景:在地图中,经常用经纬度坐标来标记位置信息,可以使用数组来记录坐标,那么该数组中只有两个元素,并且这两个元素都是数值类型
1
| let position: number[] = [39.5427, 116.2317]
|
使用 number[] 缺点:不严谨,因该类型的数组中可以出现任意多个数字。更好的方式:使用元素
1
| let position: [number, number] = [39.5427, 116.2317]
|
元组类型可以确切地标记出有多少个元素,以及每个元素的类型
类型推论
如果没有明确的指定类型,那么 TypeScript 会依照类型推论(Type Inference)的规则推断出一个类型
发生类型推论的两种场景场景:
- 声明变量并初始化时
- 决定函数返回值时
这两种情况下,类型注解可以省略不写
1 2
| let age = 18 function add(num1: number, num2: number) { return num1 + num2 }
|
类型断言
类型断言(Type Assertion)可以用来手动指定一个值的类型,类型断言有两种语法:
或
在 tsx 语法(React 的 jsx 语法的 ts 版)中必须使用前者,即 值 as 类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| interface Cat { name: string; run(): void; } interface Fish { name: string; swim(): void; }
function isFish(animal: Cat | Fish) { if (typeof (animal as Fish).swim === 'function') { return true; } return false; }
|
字面量类型 Literal Types
1 2
| let str1 = 'Hello TS' const str2 = 'Hello TS'
|
此处的 str2 的类型为 'Hello TS',而不是 string,因为 str2 是一个字面量类型,它的值只能是 'Hello TS',不能是其他值
除字符串外,任意的 JS 字面量(对象,数字等)都可以作为类型使用
使用场景:用来表示一组明确的可选值列表,字面量类型配合联合类型一起使用,比如在贪吃蛇游戏中,游戏方向的可选值只能是上,下,左,右中的任意一个:
1 2 3
| function changeDirection(direction: 'up' | 'down' | 'left' | 'right') { console.log(direction) }
|
相比于 string 类型,使用字面量类型更近精确,严谨
枚举类型 Enum
枚举(Enum)类型用于取值被限定在一定范围内的场景,比如一周只能有七天,颜色限定为红绿蓝等
1 2 3 4 5 6 7 8
| enum Direction { Up, Down, Left, Right }
function changeDirection(direction: Direction) { console.log(direction) }
changeDirection(Direction.Up)
|
数字枚举
枚举成员会被赋值为从 0 开始递增的数字,同时也会对枚举值到枚举名进行反向映射,也可以给枚举中的成员初始化值
1 2
| enum Direction { Up = 10, Down, Left, Right }
|
1
| enum Direction { Up = 2, Down = 4, Left = 8, Right = 16 }
|
字符串枚举
1 2 3 4 5 6
| enum Direction { Up = 'UP', Down = 'DOWN', Left = 'LEFT', Right = 'RIGHT' }
|
字符串枚举没有自增长行为,因此,字符串枚举的每个成员必须有初始值
当编译为 JS 代码时,其他类型会在编译时自动移除,但是枚举类型会被保留在 JS 代码中
1 2 3 4 5 6 7
| var Direction; (function (Direction) { Direction["Up"] = "UP"; Direction["Down"] = "DOWN"; Direction["Left"] = "LEFT"; Direction["Right"] = "RIGHT"; })(Direction || (Direction = {}));
|
枚举与字面量类型+联合类型组合的功能类似,都用了表示一组明确的可选值列表。一般情况下,推荐使用字面量类型+联合类型组合的方式,因为相比枚举,这种方式更近直观,简洁,高效
any 类型
不推荐使用 any!这会让 TypeScript 失去 TS 类型保护的优势
任意值(Any)用来表示允许赋值为任意类型
1 2
| let myFavoriteNumber: any = 'seven'; myFavoriteNumber = 7;
|
其他隐式具有 any 的情况:1. 声明变量不提供类型也不提供默认值 2. 函数参数不加类型
typeof
JS 中提供了 typeof 操作符,用来在 JS 中获取数据的类型
1
| console.log(typeof "Hello world")
|
实际上,TS 也提供了 typeof 操作符:可以在类型上下文中引用变量或属性的类型
使用场景:根据已有变量的值,获取该值的类型,来简化类型书写
1 2 3 4 5
| let p = { x: 1, y: 2 } function formatPoint(point: { x: number, y: number} ) { console.log(point.x.toFixed(2), point.y.toFixed(2)) } formatPoint(p)
|
1 2 3 4
| let p = { x: 1, y: 2 } function formatPoint(point: p ) { console.log(point.x.toFixed(2), point.y.toFixed(2)) }
|
- 使用
typeof 操作符来获取变量 p 的类型,结果与第一种(对象字面量形式的类型)相同
typeof 出现在类型注解的位置(参数名称的冒号后面)所处的环境就在类型上下文(区别于 JS 代码)
- 注意:
typeof 只能用来查询变量或属性的类型,无法查询其他形式的类型(比如函数调用的类型)
TypeScript 进阶
类 Class
TypeScript 全面支持 ES2015 中引入的 class 关键字,并为其添加了类型注解和其他语法(比如,可见性修饰符等)
类相关的概念:
类(Class):定义了一件事物的抽象特点,包含它的属性和方法
对象(Object):类的实例,通过 new 生成
面向对象(OOP)的三大特性:封装、继承、多态
封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,同时也保证了外界无法任意更改对象内部的数据
继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性
多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如 Cat 和 Dog 都继承自 Animal,但是分别实现了自己的 eat 方法。此时针对某一个实例,我们无需了解它是 Cat 还是 Dog,就可以直接调用 eat 方法,程序会自动判断出来应该如何执行 eat
存取器(getter & setter):用以改变属性的读取和赋值行为
修饰符(Modifiers):修饰符是一些关键字,用于限定成员或类型的性质。比如 public 表示公有属性或方法
抽象类(Abstract Class):抽象类是供其他类继承的基类,抽象类不允许被实例化。抽象类中的抽象方法必须在子类中被实现
接口(Interfaces):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(implements)。一个类只能继承自另一个类,但是可以实现多个接口
实例属性初始化:
1 2 3 4
| class Person { age: number gender = 'male' }
|
- 声明成员 age,类型为 number,没有初始值
- 声明成员 gender,并设置初始值,此时,可省略类型注解(TS 类型推论为 string 类型)
构造函数
1 2 3 4 5 6 7 8 9 10 11 12
| class Person { age: number gender: string
constructor(age: number, gender: string) { this.age = age this.gender = gender } }
const p = new Person(18, 'male') console.log(p.age, p.gender)
|
- 成员初始化后,才可以通过 this.age 来访问实例成员
- 需要为构造函数指定类型注解,否则会被隐式推断为 any;构造函数不需要返回值类型
实例方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class Point { x = 1 y = 2
scale(n: number): void { this.x *= n this.y *= n } }
const p = new Point() p.scale(10)
console.log(p.x, p.y)
|
类的继承
类继承的两种方式:
extends 继承父类
implements 实现接口
JS 中只有 extends,而 implements 是 TS 提供的
1 2 3 4 5 6 7 8 9 10 11 12 13
| class Animal { move() { console.log('move') } }
class Dog extends Animal { bark() { console.log('bark') } }
const dog = new Dog()
|
- 通过
extends 关键字实现类的继承
- 子类 Dog 继承了父类 Animal ,则 Dog 的实例对象就同时具有了父类 Animal 和子类 Dog 的所有属性和方法
1 2 3 4 5 6 7 8 9 10 11
| interface ISingable { name: string sing(): void }
class Person implements ISingable { name = 'Zhang' sing() { console.log('sing') } }
|
- 通过
implements 关键字让 class 实现接口
- Person 类实现 Singable 意味着 Person 类中必须实现 ISingable 接口中的所有方法和属性
可见性修饰符
类成员可见性:可以使用 TS 来控制 class 的方法或属性对于 class 外的代码是否可见
TypeScript 可以使用三种访问修饰符(Access Modifiers),分别是 public、private 和 protected
public:修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的
private:修饰的属性或方法是私有的,不能在声明它的类的外部访问(对子类和实例对象都不可见)
protected:修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的,对实例不可见
参数属性
修饰符和 readonly 还可以使用在构造函数参数中,等同于类中定义该属性同时给该属性赋值,使代码更简洁
只读属性关键字 readonly,只允许出现在属性声明或索引签名或构造函数中,用来防止在构造函数之外对属性进行赋值
1 2 3 4 5 6
| class Person { readonly age: number = 18 constructor(age: number) { this.age = age } }
|
- 使用
readonly关键字修饰该属性是只读的,注意只能修饰属性不能修饰方法
- 注意:属性 age 后面的类型注解(比如此处的 number)如果不加,则 age 的类型为 18(字面量类型)
- 接口或者 {} 表示的对象类型,也可以使用 readonly
- 如果 readonly 和其他访问修饰符同时存在的话,需要写在其后面
类型兼容性
两种类型系统:
- 结构化类型系统 Structural Type System
- 标明类型系统 Nominal Type System
TS 采用的是结构化类型系统,也叫做 duck typing(鸭子类型),类型检查关注的是值所具有的形状
1 2 3 4
| class Point { x: number; y: number } class Point2D { x: number; y: number }
const p: Point = new Point2D()
|
- Point 和 Point2D 是两个名称不同的类,变量 p 的类型被显示标注为 Point 类型,但是,它的值却是 Point2D 的实例,并且没有类型错误
- 因为 TS 是结构化类型系统,只检查Point 和 Point2D 的结构是否相同(都具有 x 和 y 两个属性,属性类型也相同)
- 但是,如果在 标明类型系统中(C#,Java 等),它们是不同的类,类型无法兼容
在结构化类型系统中,对于对象类型来说,y 的成员至少与 x 相同。则 x 兼容 y(成员多的可以赋值给少的)
1 2 3 4
| class Point { x: number; y: number } class Point3D { x: number; y: number; z: number }
const p: Point = new Point3D()
|
- Point3D 的成员至少与 Point 相同,则 Point 兼容 Point3D
- 因此,成员多的 Point3D 可以赋值给成员少的 Point
除了 class 之外,TS 中的其他类型也存在相互兼容的情况,包括
- 接口兼容性
- 函数兼容性等
接口兼容性
接口之间的兼容性类似于 class,并且 class 和 interface 之间也可以兼容
1 2 3 4 5 6 7 8 9
| interface Point { x: number; y: number } interface Point2D { x: number; y: number }
let p1: Point let p2: Point2D = p1
interface Point3D { x: number; y: number; z: number } let p3: Point3D p2 = p3
|
1 2 3
| interface Point2D { x: number; y: number } class Point3D { x: number; y: number; z: number } let p3: Point2D = new Point3D()
|
函数兼容性
函数兼容性需要考虑:
- 参数个数
- 参数类型
- 返回值类型
参数个数:参数多的兼容参数少的(参数少的可以赋值给多的)
1 2 3 4
| type F1 = (a: number) => void type F2 = (a: number, b: number) => void let f1: F1 let f2: F2 = f1
|
1 2 3
| const arr = ['a', 'b', 'c'] arr.forEach(() => {}) arr.forEach(((item) => {})
|
- 参数少的可以赋值给多的,因此 f1 可以赋值给 f2
- 数组 forEach 方法的第一个参数是回调函数,该示例中类型为:(value: string, index: number, array: string[]) => void
- 在 JS 中省略用不到的函数参数实际上是很常见的,这样的使用方式,促成了 TS 中函数类型之间的兼容性
- 并且因为回调函数是由类型的,所以 TS 会自动推导出参数 item,index,array 的类型
参数类型:相同位置的参数类型要相同(原始类型)或兼容(对象类型)
1 2 3 4
| type F1 = (a: number) => string type F2 = (a: number) => string let f1: F1 let f2: F2 = f1
|
函数类型 F2 兼容函数类型 F1,因为 F1 和 F2 的第一个参数类型相同
1 2 3 4 5 6
| interface Point2D { x: number; y: number } interface Point3D { x: number; y: number; z: number } type F2 = (p: Point2D) => void type F3 = (p: Point3D) => void let f2: F2 let f3: F3 = f2
|
特别注意,此处与上述提到的接口兼容性冲突,这是参数少的 f2 可以赋值给参数多的 f3
返回值类型:只关注返回值类型本身即可
1 2 3 4
| type F5 = () => string type F6 = () => string let f5: F5 let f6: F6 = f5
|
- 如果返回值类型是原始类型,此时两个类型要相同
1 2 3 4 5
| type F7 = () => { name: string } type F8 = () => { name: string; age: number } let f7: F7 let f8: F8 f7 = f8
|
- 如果返回值类型是对象类型,此时成员多的可以赋值给成员少的
交叉类型
交叉类型(&):功能类似于接口继承(extends),用于组合多个类型为一个类型(常用于对象类型)
1 2 3 4 5 6 7
| interface Person { name: string } interface Contact { phone: string } type PersonDetail = Person & Contact let obj: PersonDetail = { name: "Zhang", phone: "123456" }
|
使用交叉类型后,新的类型 PersonDetail 就同时具备了 Person 和 Contact 的所有属性类型,相当于
1
| type PersonDetail = { name: string; phone: string }
|
交叉类型与接口继承的对比
- 相同点:都可以实现对象类型的组合
- 不同点:两种方式实现类型组合时,对于同名属性之间,处理类型冲突的方式不同
泛型 Generics
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性
泛型在**保证类型安全(不丢失类型信息)**的同时,可以让函数等与多种不同的类型一起工作,灵活可复用
1 2 3 4 5 6 7 8 9 10 11
| function createArray<T>(length: number, value: T): Array<T> { let result: T[] = []; for (let i = 0; i < length; i++) { result[i] = value; }
return result; }
createArray<string>(3, 'x'); createArray(3, 'x');
|
- 语法:在函数名称的后面添加
<>,尖括号中添加类型变量
- 上例中,我们在函数名后添加了
<T>,其中 T 用来指代任意输入的类型,在后面的输入 value: T 和输出 Array<T> 中即可使用了
- 接着在调用的时候,可以指定它具体的类型为 string。当然,也可以不手动指定,而让类型推论自动推算出来
多个类型参数
定义泛型的时候,可以一次定义多个类型参数
1 2 3 4 5
| function swap<T, U>(tuple: [T, U]): [U, T] { return [tuple[1], tuple[0]]; }
swap([7, 'seven']);
|
泛型约束
泛型约束:默认情况下,泛型函数的类型变量 Type 可以代表多个类型,这导致无法访问任何属性
1 2 3 4 5 6
| function loggingIdentity<T>(arg: T): T { console.log(arg.length); return arg; }
|
这时,我们可以对泛型进行约束,只允许这个函数传入那些包含 length 属性的变量。这就是泛型约束:
1 2 3 4 5 6 7 8
| interface Lengthwise { length: number; }
function loggingIdentity<T extends Lengthwise>(arg: T): T { console.log(arg.length); return arg; }
|
- 使用
extends 关键字约束了泛型 T 必须符合接口 Lengthwise 的形状,也就是必须包含 length 属性
- 此时如果调用 loggingIdentity 的时候,传入的 arg 不包含 length,那么在编译阶段就会报错了
TypeScript 核心概念综合示例
下面是一个包含 TypeScript 所有核心概念的完整示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381
|
interface Movable { move(): string; getSpeed(): number; }
interface Vehicle extends Movable { start(): void; stop(): void; }
enum AnimalType { Dog = "DOG", Cat = "CAT", Bird = "BIRD" }
enum Age { Young = 0, Adult = 5, Senior = 10 }
type StringOrNumber = string | number; type AnimalName = "旺财" | "小咪" | "唐老鸭";
type Timestamp = { createdAt: Date; updatedAt: Date; };
type WithTimestamp<T> = T & Timestamp;
abstract class Animal { private _name: string; protected _age: number; readonly id: number; static count: number = 0;
constructor(name: string, age: number) { this._name = name; this._age = age; this.id = ++Animal.count; }
get name(): string { return this._name; }
set name(value: string) { if (!value) throw new Error("名字不能为空"); this._name = value; }
get age(): number { return this._age; }
abstract makeSound(): string; abstract eat(): string;
sleep(): string { return `${this.name} 正在睡觉`; }
static getCount(): number { return Animal.count; } }
class Dog extends Animal implements Vehicle { public breed: string; private speed: number = 0;
constructor(name: string, age: number, breed: string) { super(name, age); this.breed = breed; }
makeSound(): string { return `${this.name} 汪汪叫`; }
eat(): string { return `${this.name} 正在吃骨头`; }
sleep(): string { return `${super.sleep()},睡得很香`; }
move(): string { return `${this.name} 正在奔跑,速度: ${this.speed}km/h`; }
getSpeed(): number { return this.speed; }
start(): void { this.speed = 30; console.log(`${this.name} 开始奔跑`); }
stop(): void { this.speed = 0; console.log(`${this.name} 停止奔跑`); }
bark(): void; bark(times: number): void; bark(message: string): void; bark(param?: number | string): void { if (typeof param === "number") { for (let i = 0; i < param; i++) { console.log(`${this.name} 汪!`); } } else if (typeof param === "string") { console.log(`${this.name} 说: ${param}`); } else { console.log(`${this.name} 汪!`); } } }
class Cat extends Animal { constructor(name: string, age: number) { super(name, age); }
makeSound(): string { return `${this.name} 喵喵叫`; }
eat(): string { return `${this.name} 正在吃鱼`; } }
class AnimalShelter<T extends Animal> { private animals: T[] = [];
add(animal: T): void { this.animals.push(animal); }
getAll(): T[] { return [...this.animals]; }
findByName(name: string): T | undefined { return this.animals.find(a => a.name === name); }
map<U>(fn: (animal: T) => U): U[] { return this.animals.map(fn); } }
function createAnimalPair<T extends Animal, U extends Animal>( first: T, second: U ): [T, U] { return [first, second]; }
function isDog(animal: Animal): animal is Dog { return animal instanceof Dog; }
function describeAnimal(animal: Animal): void { console.log(`动物: ${animal.name}, 年龄: ${animal.age}`);
if (isDog(animal)) { console.log(` 这是一只 ${animal.breed} 品种的狗`); animal.bark(); } else if (animal instanceof Cat) { console.log(" 这是一只猫"); } }
type PartialDog = Partial<Dog>;
type RequiredDog = Required<Dog>;
type ReadonlyDog = Readonly<Dog>;
type DogNameAndBreed = Pick<Dog, "name" | "breed">;
type DogWithoutBreed = Omit<Dog, "breed">;
type AnimalRegistry = Record<string, Animal>;
function sealed(constructor: Function) { Object.seal(constructor); Object.seal(constructor.prototype); }
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; descriptor.value = function (...args: any[]) { console.log(`调用方法: ${propertyKey},参数:`, args); const result = originalMethod.apply(this, args); console.log(`方法 ${propertyKey} 返回:`, result); return result; }; return descriptor; }
function readonly(target: any, propertyKey: string) { Object.defineProperty(target, propertyKey, { writable: false }); }
type AnimalProperties = { [K in keyof Animal]: Animal[K]; };
type IsString<T> = T extends string ? true : false; type Test1 = IsString<string>; type Test2 = IsString<number>;
namespace AnimalUtils { export function getAnimalInfo(animal: Animal): string { return `${animal.name} (${animal.age}岁)`; }
export function compareAge(a: Animal, b: Animal): number { return a.age - b.age; } }
async function fetchAnimalData(id: number): Promise<Animal | null> { return new Promise((resolve) => { setTimeout(() => { resolve(new Dog("远程狗", 3, "柴犬")); }, 1000); }); }
async function main(): Promise<void> { console.log("=== TypeScript 核心概念综合演示 ===\n");
console.log("1. 类和对象:"); const dog = new Dog("旺财", 3, "金毛"); const cat = new Cat("小咪", 2); console.log(`创建了: ${dog.name}`); console.log(`创建了: ${cat.name}`);
console.log("\n2. Getter/Setter:"); console.log(`狗的名字: ${dog.name}`); dog.name = "大黄"; console.log(`修改后: ${dog.name}`);
console.log("\n3. 抽象类和多态:"); const animals: Animal[] = [dog, cat]; animals.forEach(animal => { console.log(animal.makeSound()); console.log(animal.eat()); });
console.log("\n4. 接口实现:"); const vehicle: Vehicle = dog; vehicle.start(); console.log(vehicle.move()); vehicle.stop();
console.log("\n5. 类型守卫:"); describeAnimal(dog);
console.log("\n6. 泛型:"); const shelter = new AnimalShelter<Dog>(); shelter.add(dog); shelter.add(new Dog("小黑", 2, "哈士奇")); console.log(`收容所有 ${shelter.getAll().length} 只狗`);
const names = shelter.map(animal => animal.name); console.log(`名字列表: ${names.join(", ")}`);
console.log("\n7. 枚举:"); console.log(`动物类型: ${AnimalType.Dog}`); console.log(`年龄阶段: ${Age.Adult}`);
console.log("\n8. 联合类型:"); const value: StringOrNumber = "Hello"; console.log(`值: ${value}`);
console.log("\n9. 静态成员:"); console.log(`动物总数: ${Animal.getCount()}`);
console.log("\n10. 可选链:"); const foundDog = shelter.findByName("旺财"); console.log(`找到的狗: ${foundDog?.name ?? "未找到"}`);
console.log("\n11. 异步处理:"); const remoteAnimal = await fetchAnimalData(1); if (remoteAnimal) { console.log(`获取到远程动物: ${remoteAnimal.name}`); }
console.log("\n12. 命名空间:"); console.log(AnimalUtils.getAnimalInfo(dog));
console.log("\n13. 方法重载:"); dog.bark(); dog.bark(3); dog.bark("你好!");
console.log("\n14. 元组:"); const pair = createAnimalPair(dog, cat); console.log(`动物对: ${pair[0].name} 和 ${pair[1].name}`);
console.log("\n15. 只读属性:"); console.log(`Dog ID: ${dog.id}`);
console.log("\n=== 演示完成 ==="); }
main().catch(console.error);
|
这个示例代码包含了以下所有 TypeScript 核心概念:
- 接口 (Interface):Movable、Vehicle 接口定义
- 接口继承:Vehicle extends Movable
- 类 (Class):Animal、Dog、Cat 类
- 抽象类:abstract class Animal
- 继承:Dog/Cat extends Animal
- 访问修饰符:public、private、protected
- Getter/Setter:属性访问器
- 只读属性:readonly
- 静态成员:static 属性和方法
- 抽象方法:abstract 关键字
- 接口实现:implements 关键字
- 泛型:泛型类和泛型函数
- 泛型约束:T extends Animal
- 枚举 (Enum):AnimalType、Age
- 类型别名 (Type Alias):type 关键字
- 联合类型:string | number
- 交叉类型:T & Timestamp
- 类型守卫:animal is Dog
- instanceof:类型判断
- 可选链 (?.) 和 空值合并 (??)
- 非空断言 (!)
- 方法重载:多个函数签名
- Promise 和 async/await:异步处理
- 元组 (Tuple):[T, U]
- 实用工具类型:Partial、Pick、Omit、Record 等
- 装饰器:类装饰器、方法装饰器
- 映射类型:[K in keyof T]
- 条件类型:T extends U ? X : Y
- 命名空间:namespace
- 类型推断:自动推导类型
参考