Hi~,我是 一碗周,一个在舒适区垂死挣扎的前端,如果写的文章有幸可以得到你的青睐,万分有幸~
写在前面
TypeScript现在几乎已经是所有前端程序员必备的技能了,现在的各大框架已经全部采用TS进行开发。本篇文章总结了TS中常用的知识点,希望能对你有所帮助。
一、类型
1. 基础类型
TypeScript中的基础类型主要有布尔(boolean
)、数字(number
)、字符串(string
)、Any
(任意类型)、Void
(无类型)、Null
、 Undefined
和Never
(无值类型),如下代码展示了TS基本类型的定义:
;(function () {
/*
* 在 TS 中可以通过 let 变量名: 数据类型 = 变量值值得方式定义数据类型(类型注解)
* 也可以在定义的时候不指定数据类型,TS 自身会推断该数据类型
*/
// 布尔型
let boo: boolean = false // 赋值非布尔值将会抛出异常
// 数字类型
let num: number = 100
// 字符串
let str: string = '字符串' // 使用单引号或者双引号定位
str = `模板字符串` // 使用模板字符串定义
// Any 类型 -> 表示该类型可以为动态的类型,该类型在编译的时候移除了类型检查
let AnyType: any = 123
AnyType = true // 重复赋值不会抛出异常
// Void 类型 -> 通常用于没有返回值的函数类型
function demo(): void {
console.log('测试void类型')
}
demo()
// 还有两个比较特殊的类型,就是 null 和 undefined
// 这两个类型是所有类型的子类型,也就是说可以将这两个两个类型赋值给 number、string 等类型
let u: undefined = undefined
num = u // 将 number 类型的变量赋值为 undefined
let n: null = null
boo = n // 将 boolean 类型的变量赋值为 null
})()
基础类型比较简单,特别类似于JavaScript,简单看来就是比JavaScript多了一个类型定义
TS 中还有一个Never类型。该类型表示的是那些永远不会存在的值得类型。例如,never
类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。
2. 数组 类型
TypeScript中的数组 与JavaScript中的数组不同,在TS中使用数组不仅可以将一个变量定义为数组,也可以定位数组中的类型。示例代码如下:
;(function () {
// 定义一个仅仅为数字的数组类型
let arr1: number[] = [1, 2, 3]
console.log(arr1)
// 定义一个可以为 数字 字符串 布尔值的数组
let arr2: (number | string | boolean)[] = ['1', '2', true]
console.log(arr2)
// 定义一个任意类型的数组
let arr3 = [1, ['1', '2', true], true]
console.log(arr3)
// 定义个对象类型的数组,对象中必须有 name 和 age 两个属性
const objectArray: { name: string; age: number }[] = [
{ name: '一碗周', age: 18 },
]
// 或者通过 type alias 类型别名的方式声明
// 通过 type 定义一个类型别名
type User = { name: string; age: number }
const objectArr: User[] = [{ name: '一碗周', age: 18 }]
})()
3. 元组类型
元组类型 允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。 示例代码如下所示:
;(function () {
// 定义一个值分别被 string 和 number 的元组
let tuple: [string, number] = ['123', 123]
console.log(tuple) // [ '123', 123 ]
// 通过索引赋值
tuple[0] = 'string'
console.log(tuple) // [ 'string', 123 ]
// 赋值其他类型
// tuple[0] = true
// console.log(tuple) // 抛出异常
})()
元组的主要作用就是约束数组中的每一项,及其数组的长度。
元组和数组是可以可以嵌套的,语法结构如下所示:
// 元组和数组的嵌套
let tuples: [string, number][] = [
['123', 123],
['456', 456],
]
上面的代码中,[string, number]
表示一个元组,在后面增加[]
,就表示这个一个存放元组的数组。
4. 对象类型
一个对象中可以包含以上所有的类型,示例代码如下所示:
;(function () {
// 定义一个对象,里面包含 MyName 和 age 两个属性,其中 MyName 为 string 类型 age 为number 类型
let obj: {
MyName: string
age: number
}
// 对象的赋值,如果不按上面指定的类型进行赋值会抛出异常
obj = {
MyName: '一碗周',
age: 18,
}
console.log(obj) // { MyName: '一碗周', age: 18 }
})()
在TS中我们并不需要在每个地方标注类型,因为类型推断 可以帮助我们在不编写额外代码的,即可获得其功能。但是你想让你的代码可读性更好一些,可以写上每个的类型。
5. 类型推断
在TypeScript中有的时候并需要明确指定类型,编译器会自动推断出合适的类型,比如下面这段代码:
;(function () {
let myName = '一碗周'
myName = true // 错误:不能将类型“boolean”分配给类型“string”
})()
我们定义myName
变量时,并没有指定其数据类型,只是为他赋了一个字符串的值,但是我们如果将这个值重新赋值为要给非string
类型的值时,编译器将会抛出异常。
这就是TypeScript中最简单的类型推断,根据右侧的值来推测变量的数据类型。
6. 类型断言
所谓的类型断言就是你来告诉TS这个值得数据类型就是某样的,你不需要做检查。
这样做得话他在运行是不会影响,只有在编译的时候回影响。示例代码如下:
let SomeValue: any = 'this is a string'
// 语法一
let StrLength1: number = (<string>SomeValue).length
// 语法二 as 语法
let StrLength2: number = (SomeValue as string).length
值得注意的是,在TS中使用JSX时,只有第二种语法是被支持的。
7. 枚举类型
所谓的枚举类型就是为一组数值赋予名字。
enum
类型在C++、Java语言中比较常见,TypeScript在JavaScript原有的类型基础上也增加了enum
类型。
比如我们需要定义一组角色,需要使用数字表示,就可以使用如下代码定位:
enum role{
STUDENT,
TEACHER,
ADMIN
}
上面代码中我们定义了
role
为一个枚举类型,这个里面有是三个值,TypeScript会为每个值自动的分配序号,默认从0开始依次排列,它们的值依次为0 1 2。
当然我们也可以自定义每个值,如果非全部定义则后面的值会根据前面的值自增。示例代码如下:
enum role1 {
student = 1,
// 后面两个值依次为2 3
teacher,
admin,
}
enum role2 {
// 每个名具有指定的值
student = 1,
teacher = 3,
admin = 6,
}
8. 联合类型
所谓的联合类型就是定义一些类型,定义的变量只需要满足任意一种类型即可,联合类型使用|
定义,示例代码如下:
// 通过 | 符号定义联合类型
let value: number | boolean | string = '一碗周'
value = 18
在上面的代码中我们定义了一个value
变量,该变量可以是number
、boolean
或者string
类型。
9. 交叉类型
介绍了联合类型,然后介绍一下与之特别相似的交叉类型。
所谓的交叉类型就是需要满足所有类型,交叉类型使用&
符号定义。示例代码如下:
// 定义三个普通接口类型
interface Name {
name: string
}
interface Age {
age: number
}
interface Hobby {
hobby: string
}
// 定义一个对象,该对象为上面三个对象的联合类型
let person: Name & Age & Hobby = {
// 如果少分配一个类型将会抛出异常
name: '一碗周',
age: 18,
hobby: 'coding',
}
10. 类型保护
现在我们有一个需求:获取一个具有任意类型数组中第一个数字。
实现代码如下:
// 定义一个包含number或者string的数组
const arr: (number | string)[] = [1, '数字']
// 遍历数组返回第一个数字
const getValue: (arr: (number | string)[]) => number = (
arr: (number | string)[],
): number => {
for (let i = 0; i < arr.length; i++) {
// 如果当前值转换为数字时候不是一个 NaN 则返回
if (!isNaN(Number(arr[i]))) {
return arr[i] // 不能将类型“string | number”分配给类型“number”。
}
}
}
上述代码中return
时并不知道返回的是不是一个number
类型。所以将会抛出异常。
上述功能可以通过类型断言来完成,示例代码如下:
const getValue: (arr: (number | string)[]) => number = (
arr: (number | string)[],
): number => {
for (let i = 0; i < arr.length; i++) {
// 如果当前值转换为数字时候不是一个 NaN 则返回
if (!isNaN(Number(arr[i]))) {
return arr[i] as number // 告诉 编译器 我返回的就是一个 number
}
}
}
如果使用类型断言来说明,如果想要的数据类型不一样时,就会显得比较繁琐。这个时候就需要类型保护来完成该功能,类型保护主要分为以下三种:
- 自定义类型保护
typeof
类型保护instanceof
类型保护
自定义类型保护
自定义类型保护的方式就是定义一个函数,该函数是的返回结构是一个parameterName is type
的形式,该形式是一个类型谓词 。parameterName
必须是来自于当前函数参数里的一个参数名。示例代码如下:
// 使用自定义类型保护
// 1. 定义一个函数 其返回值是一个 类型谓词,即 parameterName is Type 也就是 参数名 is 类型 的形式
function isNumber(value: number | string): value is number {
// 如果返回 true 则说明 传入的 value 是 is 后面的type
return !isNaN(Number(value))
}
// 2. 定义一个获取数字的函数
const getNumber: (value: number | string) => number = (
value: number | string,
): number => {
// 如果调用 isNumber 的值为 true 则说明 value 是一个数字,所以将数字返回
if (isNumber(value)) {
return value
}
}
// 3. 调用获取最终的数值
const getValue: (arr: (number | string)[]) => number = (
arr: (number | string)[],
): number => {
for (let i = 0; i < arr.length; i++) {
// 如果返回数字,转换为 boolean 值为 true
if (getNumber(arr[i]) || getNumber(arr[i]) === 0) {
return getNumber(arr[i])
}
}
}
定义第二个函数的原因是在数组中直接使用i
作为返回值还是有问题的,所以定义一个函数过渡一下。
typeof
类型保护
JavaScript 中的typeof
关键字可以判断当前类型,但是仅仅只能判断number
、string
、boolean
和symbol
四种类型。
在这个需求中就足够了,接下来我们看看如何通过typeof
来实现类型保护。示例代码如下:
// 1. 定义一个获取数字的函数
const getNumber: (value: number | string) => number = (
value: number | string,
): number => {
// 判断当前是否为字符串,如果是返回当前值
if (typeof value === 'number') {
return value
}
}
// 2. 调用获取最终的数值
const getValue: (arr: (number | string)[]) => number = (
arr: (number | string)[],
): number => {
for (let i = 0; i < arr.length; i++) {
// 如果返回数字,转换为 boolean 值为 true
if (getNumber(arr[i]) || getNumber(arr[i]) === 0) {
return getNumber(arr[i])
}
}
}
instanceof
类型保护
instanceof
操作符也是JavaScript中提供的原生操作符,它用来判断一个实例是不是某个构造函数创建的,或者是不是使用ES6语法的某个类创建的。在TypeScript中也可以通过instanceof
操作符来实现类型保护,示例代码如下:
/**
* 由于 instanceof 仅仅支持引用类型,不支持原始类型,所以说这里需要进行一下改动,将数组修改为如下:
*/
const arr2: (Number | String)[] = [new String('彼岸繁華'), new Number(10)]
// 1. 定义一个获取数字的函数
const getNumber: (value) => number = (value): number => {
// 判断当前是否为 Number 类型,将当前值转换为字符串返回
if (value instanceof Number) {
return Number(value)
}
}
// 2. 调用获取最终的数值
const getValue: (arr: (Number | String)[]) => number = (
arr: (Number | String)[],
): number => {
for (let i = 0; i < arr.length; i++) {
// 如果返回数字,转换为 boolean 值为 true
if (getNumber(arr[i]) || getNumber(arr[i]) === 0) {
return getNumber(arr[i])
}
}
}
使用instanceof
时需要注意一下两点:
- 只适应于任何引用类型,不支持原始类型。
- 前者的原型链上是否
包含
后者的原型对象。
二、函数
11. 函数定义
TS中的函数声明方式与JS类似,唯一不同的就是固定了参数类型了返回值类型,如果没有返回值其返回值类型必须为void
而不是留空。
接下来用 TS 的方式重新声明以上是三个函数:
第一种:使用function
关键字声明函数:
/*
语法结构如下
function 函数名(形参1: 类型, 形参2: 类型,...): 返回值类型 {
函数体
}
*/
function add4(x: number, y: number): number {
return x + y
}
第二种:使用字面量方式声明函数
/*
语法结构如下
const 函数名 = function (形参1: 类型, 形参2: 类型,...): 返回值类型 {
函数体
}
*/
const add5 = function (x: number, y: number): number {
return x + y
}
第三种:使用箭头函数声明函数
/*
语法结构如下
const 函数名 = (形参1: 类型, 形参2: 类型,...): 返回值类型 => {
函数体
}
*/
// 3. 使用箭头函数声明函数
const add6 = (x: number, y: number): number => {
return x + y
}
以上就是在TS中声明函数的方式。JS中还有一种参数解耦赋值的情况,这种在TS中怎么指定参数类型呢?示例代码如下:
const add7 = ({ x, y }: { x: number; y: number }): number => {
return x + y
}
在TS中还有一种可读性更高的写法,如下所示:
const add8: (baseValue: number, increment: number) => number = function (
x: number,
y: number
): number {
return x + y
}
这种方式将函数分为两个部分,=
前面是函数的类型的返回值类型,后半部分才是函数定义的地方。
其实前半部分也就是为了增加代码的可读性,没有太大的实际意义。
12. 可选参数和默认参数
TypeScript 里的每个函数都是必须。这并不代表不能传递null
和undefined
作为参数,而是是否为每个参数都传递了值,如果不一一对应,则会抛出异常。简单的说就是形参个数与实参个数一致。
示例代码如下所示:
function add(x: number, y: number): number {
return x + y
}
let result1 = add(1) // Expected 2 arguments, but got 1.
let result2 = add(1, 2)
let result3 = add(1, 2, 3) // Expected 2 arguments, but got 3
在JS中每个参数都是可选的,可传递也可不传递,如果不传递的时候,它将是默认的undefined
。
在TS中也是可以实现的,我们只需要在参数名后面添加 ?
即可实现可选参数 的功能。如下代码
// 实现可选参数功能
// 参数名旁加一个?即可
function add(x: number, y?: number): number {
return x + y
}
let result1 = add(1)
let result2 = add(1, 2)
// let result3 = add(1, 2, 3) // Expected 2 arguments, but got 3
如上代码就实现了 可选参数
在TS中实现默认参数与JS实现默认参数是相同的,只需要为其赋值即可。示例代码如下所示:
;(function () {
function add(x: number, y: number = 2): number {
return x + y
}
let result1 = add(1) // 3
let result2 = add(1, 2) // 3
})()
当然,如果不为
y
指定类型就与JS中一样一样了。
13. 剩余参数
所谓的剩余参数就是函数定义时需要传递两个参数,而函数调用时传递了3个参数;此时就多余出一个参数,该参数就是剩余参数。
在 JS 中我们可以使用arguments
来访问多余传递的参数。那在TS中怎么访问剩余参数呢?
实际上TS中可以将所有的参数存储在一个变量中,该变量实际上一个解耦的一个数组。示例代码如下:
function fun(x: number, ...numbers: number[]): void {
console.log(numbers)
}
fun(1, 2, 3, 4) // [ 2, 3, 4 ]
三、接口
TS的核心原则之一就是对所具有的结构 进行类型检查。接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。
最终被编译成JavaScript代码后不包含接口以及类型约束的代码。
14. 接口定义
接口的作用于type
关键字类似,但是又不一样。type
可以定义简单的数据类型,例如如下代码
type str = string
这种写法就不能应用在 接口 中,接口中只能写函数类型和类类型还有数组类型。
在TS中定义接口使用interface
关键字。
示例代码如下所示:
// 定义一个简单的接口
interface Person {
name: string
}
// 定义 get 方法
function getPersonName(person: Person): void {
console.log(person.name)
}
// 定义 set 方法
function setPersonName(person: Person, name: string): void {
person.name = name
}
// 定义一个 person 对象
let person = {
name: '一碗粥',
}
setPersonName(person, '一碗周')
// 修改成功
getPersonName(person) // 一碗周
Person
接口就像是一个名字,它用来描述使用该接口中的要求,例如此接口中需要一个name
属性,且类型为string
类型。
值得注意的是,类型检查并不会检查属性的顺序,只需要对应你的属性存在,且类型相同即可。
15. 接口属性
可选属性
如果接口中的某个属性是可选的,或者说仅仅在某个条件下存在,可以在属性名旁边加一个?
号。示例代码如下:
;(function () {
// 定义一个简单的接口
interface Person {
name: string
// 说明 age 是可选的
age?: number
}
// 定义一个 person 对象
let person = {
name: '一碗周',
age: 18,
hobby: 'coding',
}
// 定义 get 方法
function getPersonName(person: Person): void {
// console.log(person.age, person.hobby) // Property 'hobby' does not exist on type 'Person'.
}
})()
此时的sex
属性我们可写可不写,但是hobb
属性,由于没有在接口中定义,我们调用会抛出异常。
只读属性
如果想让一个属性为一个只读属性,仅仅需要在属性前面添加readonly
即可。示例代码如下:
;(function () {
interface Person {
// 将 name 设置为只读
readonly name: string
}
// 定义一个 person 对象
let person = {
name: '一碗周',
}
// 定义 set 方法
function setPersonName(person: Person, name: string): void {
person.name = name // Cannot assign to 'name' because it is a read-only property.
}
setPersonName(person, '一碗粥')
})()
16. 接口继承
和类一样,接口也可以相互继承。 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。
接口继承使用extends
关键字,示例代码如下:
// 定义两个接口
interface PersonName {
name: string
}
interface PersonAge {
age: number
}
// 定义一个 Person 接口继承于以上两个接口 多个接口使用 , 逗号分割
interface Person extends PersonName, PersonAge {
hobby: string
// 定义一个方法,返回值为 string
say(): string
}
let person = {
name: '一碗周',
age: 18,
hobby: 'coding',
// 示例方法
say() {
return '一碗周'
},
}
// 定义 get 方法
function getPersonName(person: Person): void {
console.log(person.name, person.age, person.hobby)
}
getPersonName(person) // 一碗周 18 coding
继承多个接口中间使用,
逗号分割。
17. 对函数类型接口进行描述
在TS中接口还可以对函数类型的接口进行描述。
函数类型接口的定义就像是一个只有参数列表和返回值类型的函数定义,参数列表中每个参数都需要名字和类型 。示例代码如下所示:
interface MyAdd {
(x: number, y: number): number
}
定义完成之后我们可以像使用普通接口一样使用这个函数接口。示例代码如下所示:
let myAdd: MyAdd = (x: number, y: number): number => {
return x + y
}
上面的代码等同于如下函数定义的代码:
let myAdd: (x: number, y: number) => number = (
x: number,
y: number
): number => {
return x + y
}
四、类
18. 定义类
在TS中也是使用class
关键字来定义一个类,示例代码如下:
;(function () {
// 定义类
class Person {
// 公共属性,默认可以不写
public name: string
// 构造函数
constructor(name: string) {
// 初始化name属性
this.name = name
}
}
// 实例化类
const person = new Person('一碗周')
console.log(person.name) // 一碗周
})()
上面定义的那个类中具有一个构造函数和一个公共属性name
,在类实例化时调用了constructor
构造函数,调用对功能属性进行初始化。
上面的写法还有一种简写形式,如下所示:
;(function () {
class Person {
constructor(public name: string) {}
}
// 实例化类
const person = new Person('一碗周')
console.log(person.name) // 一碗周
})()
这个写法等同于上面那个写法。
19. 继承
在面向对象的编程语言中,有一个重要得特征就是继承。继承就是基于某个类来扩展现有的类。
例如,爸爸在北京有一个四合院,儿子可以继承爸爸的四合院,而且还可以自己去买一栋别墅;最终儿子的房产拥有北京的四合院和一栋别墅。
在TS中继承使用extends
关键字,示例代码如下:
;(function () {
// 定义一个基类,又称超类
class Person {
// 在基类中定义一个name属性
constructor(public name: string) {}
}
// 定义一个派生类,又称子类,继承于基类
class Programmer extends Person {
constructor(name: string, public hobby: string) {
// 通过 super 调用基类的构造函数
super(name)
}
}
// 实例化子类
const programmer = new Programmer('一碗周', 'coding')
console.log(programmer.name, programmer.hobby) // 一碗周 coding
})()
如上示例代码中,Person
称作基类 ,或者称超类 ,Programmer
是一个派生类 ,或者称子类 。
在上面那个例子中,Programmer
类通过extends
关键字继承于Person
类。子类拥有父类全部的属性和方法。
在子类的构造函数中,我们必须调用super()
方法来执行基类的构造函数,这个是必须的。
类的继承不仅可以继承类,而且还可以在子类重写父类的属性或者方法。实例代码如下:
// 定义一个 Person类
class Person {
constructor(public name: string) {}
// 定义一个方法
sayMy() {
console.log(`我的名字: ${this.name}`)
}
}
// 定义一个 Adult 类继承于 Person 类
class Adult extends Person {
constructor(public age: number) {
super('彼岸繁華')
} // 重写父类方法
sayMy() {
console.log(`我的名字: ${this.name} 年龄: ${this.age}`)
}
}
// 定义一个 Programmer 类继承于 Adult 类
class Programmer extends Adult {
constructor(public hobby: string) {
super(18)
} // 重写父类方法
sayMy() {
console.log(
`我的名字: ${this.name} 年龄: ${this.age} 爱好: ${this.hobby}`
)
}
}
// 类的实例化
const programmer = new Programmer('coding')
programmer.sayMy() // 我的名字: 彼岸繁華 年龄: 18 爱好: coding
首先我们定义了一个Person
类,在类中定义了一个属性和一个方法;然后又定义了一个Adult
类,这个类继承于Person
类,并重写了Person
类中的方法;最后又定义了一个Programmer
类,这个类继承于Adult
类,并重写了Adult
类中的方法,也就是说Programmer
类拥有Person
类与Adult
类中的全部属性与方法,但是sayMe()
方法被重写了两次,也就是说Programmer
类拥有3个属性和1个方法。
20. public
、private
和protected
修饰符
public
、private
、protected
修饰符的区别:
public
:公开的,我们可以在类中自由的访问类中定义的成员。TS默认为public
private
:私有的,仅仅可以在类中访问定义的成员,在类外访问不到protected
:受保护的,可以在本类或者子类中访问定义的成员。
示例代码如下所示:
// 定义一个 Person 类,其中包含 public 成员 private 成员和 protected 成员。
class Person {
public name: string
// 约定 私有成员一般采用 _ 开头
private _age: number
protected hobby: string
// 属性初始化
constructor(name: string, age: number, hobby: string) {
this.name = name
this._age = age
this.hobby = hobby
}
sayMy() {
console.log(this.name, this._age, this.hobby)
}
}
// 实例化 Person 类
const person = new Person('一碗周', 18, 'coding')
console.log(person.name) // 一碗周
// 类外访问私有成员 抛出异常
// console.log(person._age) // 报错
// 类外访问保护成员 抛出异常
// console.log(person.hobby) // 报错
// private 成员和 protected 成员可以在类内访问
person.sayMy() // 一碗周 18 coding
// 定义一个类继承与 Person 类
class Programmer extends Person {
constructor(name: string, age: number, hobby: string) {
super(name, age, hobby)
}
sayMy() {
console.log(this.name) // 一碗周
// 在子类不可以访问父类的私有成员
// console.log(this._age) // 报错
// 在子类可以访问受保护的成员
console.log(this.hobby) // coding
}
}
// 实例化 Programmer 类
const programmer = new Programmer('一碗周', 18, 'coding')
programmer.sayMy()
// 确保跟其他代码中的成员冲突
export {}
如上代码中,我们可以在基类中访问,公共成员、私有成员和保护成员,但是我们在类外只能访问公共成员。当我们定义一个子类继承于Person
类时,我们可以在子类访保护成员,但是不能访问私有成员。
21. getter与setter
类中的私有成员和保护成员我们并不是真的不能读写,在TS中提供了getters
与setters
帮助我们有效的控制对对象成员的访问。
示例代码如下所示:
// 定义一个 Person 类
class Person {
// 约定 私有成员一般采用 _ 开头
private _name: string
// 属性初始化
constructor(name: string) {
this._name = name
}
// 获取 私有的 _name 属性值
get getName(): string {
return this._name
}
// 设置 私有的 _name 属性值
set setName(name: string) {
this._name = name
}
}
// 实例化类
const person = new Person('一碗粥')
// 通过 getName 的方式获取
console.log(person.getName) // 一碗粥
// 通过 setName 的方式设置 _name 的值
person.setName = '一碗周'
// 重新获取
console.log(person.getName) // 一碗周
22. readonly
修饰符
我们可以通过 readonly
修饰符将一个属性设置为只读的。只读属性必须在声明时或者在构造函数中进行初始化。示例代码如下所示:
// 定义一个类,且具有一个只读属性
class Person {
// readonly name: string
// 等同于
// public readonly name: string
// constructor(name: string) {
// this.name = name
// }
// 或者
constructor(public readonly name: string) {}
}
// 实例化
const person = new Person('一碗周')
console.log(person.name) // 一碗周
// 修改name的值
// person.name = '一碗周' // 错误! name 是只读的.
23. 静态成员
在 TS 中我们也可以创建静态成员,这些属性或者方法是存在于类本身而不是存在于类的实例上。在 TS中定义静态成员与ES6中一样,都是使用static
关键字来说明。
示例代码如下所示:
class Hero {
// 计数器
static count = 0
constructor(public name: string) {
// 每创建一个属性 count ++
++Hero.count
}
}
// 实例一个 Hero 类
const hero1 = new Hero('孙悟空')
console.log(Hero.count) // 1
const hero2 = new Hero('哪吒')
console.log(Hero.count) // 2
这里我们用静态属性实现了一个记录实例化几个类的一个计数器。
24. 抽象类
想要理解什么是抽象类,就需要先理解什么是抽象,所谓的抽象就是从众多的事物中抽取出共同的、本质性的特征,而舍弃其非本质的特征 。例如苹果、香蕉、生梨、葡萄、桃子等,它们共同的特性就是水果。得出水果概念的过程,就是一个抽象的过程。
抽象类就是将众多类中具有共同部分的功能抽离出来,单独创建一个类作为其他派生类的基类使用。他们不允许被实例化,定义抽象类使用abstract
关键字。
抽象方法就是只有方法的定义,没有方法体,方法体需要在子类中进行实现。
示例代码如下:
// 通过 abstract 关键字 定义一个抽象类,该类不必进行初始化,仅作为基类使用
abstract class Department {
// 初始化name成员,参数属性
constructor(public name: string) {}
printName(): void {
console.log('部门名称: ' + this.name)
}
// 抽象方法必须包含 abstract 关键字
abstract printMeeting(): void // 必须在派生类中实现
}
class AccountingDepartment extends Department {
constructor() {
super('会计部') // 在派生类的构造函数中必须调用super()
}
printMeeting(): void {
console.log('会计部是负责管钱的部门')
}
}
// const department = new Department() // 抛出异常:不能创建一个抽象类的实例
// 实例化抽象子类
const department = new AccountingDepartment()
// 调用抽象类中的方法
department.printName() // 部门名称: 会计部
// 调用在派生类实现的抽象方法
department.printMeeting() // 会计部是负责管钱的部门
25. 类与接口
类定义会创建两个东西:类的实例类型和一个构造函数,因为类可以创建出类型,这一点与我们之前学习的接口类似,所以说我们可以在使用接口的地方使用类。示例代码如下所示:
// 定义一个类
class Point {
x: number
y: number
}
// 定义一个接口继承与类
interface Point3d extends Point {
z: number
}
let point3d: Point3d = { x: 1, y: 2, z: 3 }
类可以通过implement
去实现一个接口,示例代码如下:
// 定义接口
interface Eat {
eat(food: string): void
}
interface Run {
run(distance: number): void
}
// 定义类实现接口
class Person implements Eat, Run {
eat(food: string): void {
console.log(food)
}
run(distance: number) {
console.log(distance)
}
}
export {}
五、泛型
26. 用法
现在我们要定义一个join
函数,该函数的功能主要是接受两个类型一样的值,返回两个参数拼接后的值。示例代码如下:
// 所谓的泛型,通俗一点的解释就是泛指的类型
// 定义一个join函数,接受两个一样类型的参数,并将两个参数拼接后返回。
function join<T>(first: T, second: T) {
return `${first}${second}`
}
// 这里明确 T 为 string 类型
join<string>('第一', '第二') // 第一第二
// 这里通过类型推导的方式,编译器会根据传入的参数自动推断出类型
join(1, 2) // 12
定义泛型是通过<>
对尖括号来定义,我们在定义join
函数的时候,并不知道可以接受那些类型,但是可以明确的是两个类型是必须一样的,如果想要满足这样的需求,不用泛型的话解决起来是没有这么简单的。
在调用函数的时候,这里使用了两种方式,一种是直接指定类型为string
类型;另一种是通过类型推导的方式,编辑器会根据传入的参数自动帮助我们确定类型。
27. 在函数中使用泛型
在定义一个函数时,我们可以使用多个泛型,而且返回值类型也可以通过泛型指定,只要在数量上和使用方式上能对应就可以。示例代码如下:
function identity<T, Y, P>(first: T, second: Y, third: P): Y {
return second
}
// 指定类型
identity<boolean, string, number>(true, '字符串', 123) // 字符串
// 类型推断
identity('string', 123, true) // true
28. 在类中使用泛型
我们不仅可以在函数中使用泛型,还可以在类中使用泛型。示例代码如下:
class DataManager<T> {
// 定义一个类,该类中具有一个T类型的私有数组
constructor(private data: T[]) {}
// 根据索引说数组中的值
getItem(index: number): T {
return this.data[index]
}
}
const data = new DataManager(['一碗周'])
data.getItem(0) // 一碗周
而且泛型还可以继承与于某个接口,示例代码如下:
interface Item {
name: string
}
class DataManager<T extends Item> {
// 定义一个类,该类中具有一个T类型的私有数组
constructor(private data: T[]) {}
// 根据索引说数组中的值
getItem(index: number): string {
return this.data[index].name
}
}
const data = new DataManager([{ name: '一碗周' }])
data.getItem(0) // 一碗周
使用extends
可以达到一个泛型约束 的作用,就上面那个代码来说,我们必须约束传入的值必有具有一个name
属性,否则就会抛出异常。
29. 在泛型约束中使用类型参数
假如有如下需求,我们定义一个类,在类中一个私有对象,该对象中包含一些属性;然后定义一个方法,通过key
来获取其对应的值。实现代码如下:
// 定义一个接口
interface Person {
name: string
age: number
hobby: string
}
// 定义一个类
class Me {
constructor(private info: Person) {}
getInfo(key: string) {
return this.info[key]
}
}
const me = new Me({
name: '一碗周',
age: 18,
hobby: 'coding',
})
// 调用 me.getInfo() 可能会得到一个 undefined 如下示例
me.getInfo('myName') // undefined
上面的代码,如果我们调用示实例对象中的getInfo()
方法时,传入一个没有的属性,会得到一个undefined
。调用一个方法返回一个undefined
时,这并不是TypeScript中的作风。
解决该问题可以通过keyof
操作符,该关键字可以通过该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。 示例代码如下:
type myPerson = keyof Person // 'name' | 'age' | 'hobby'
那现在就可以通过该操作符解决上面出现的那个问题。示例代码如下:
class Me {
constructor(private info: Person) {}
// 该写法与如下写法是一样的
getInfo<T extends keyof Person>(key: T): Person[T] {
return this.info[key]
}
// getInfo<T extends 'name' | 'age' | 'hobby'>(key: T): Person[T] {
// return this.info[key]
// }
}
const me = new Me({
name: '一碗周',
age: 18,
hobby: 'coding',
})
// 调用 me.getInfo() 如果传递一个未知的属性则会编译错误
me.getInfo('myName') // error : 类型“"myName"”的参数不能赋给类型“keyof Person”的参数。
现在我们只要访问对象中不具有的属性编译则会异常。
六、命名空间
30. 命名空间的定义和使用
命名空间的定义就相当于定义了一个对象,该对象中可以定义变量、接口、类、方法等等,但是如果不使用export
关键字指定此内容为外部可见的话,外部是没有办法访问到的。
接下来定义一个正则验证的一个.ts
文件,实现代码如下:
// validation.ts
// 通过 namespace 创建一个名为 Validation 的命名空间
namespace Validation {
// 定义一个正则表达式
const isLetterReg = /^[A-Za-z]+$/
// 这里在定义一个正则表达式,与上一个的区别就是这个正则表达式通过export导出了
export const isNumberReg = /^[0-9]+$/
// 导出一个方法
export const checkLetter = (text: any) => {
return isLetterReg.test(text)
}
}
在上面的代码中,我们定义了一个名为Validation
的命名空间,并在里面定义了两个属性和一个方法,并将一个属性和一个方法导出(命名空间的中导出使用export
关键字)。
在某个文件使用命名空间中的内容只需要在使用外部命名空间的地方使用/// <reference path=“namespace.ts”/>
来引入,注意三斜线///
开头,然后在 path
属性指定相对于当前文件,这个命名空间文件的路径。具体代码如下:
// index.ts
/// <reference path='validation.ts' />
let isLetter = Validation.checkLetter('text')
const reg = Validation.isNumberReg
console.log(isLetter)
console.log(reg)
值得注意的是第一行的/// <reference path='validation.ts' />
。语法结构是不能错的,否则编译是不通过的。
编译命令如下:
tsc --outFile src/index.js index.ts
outFile
参数是用于将输出文件合并为一个文件
编译后的index.js
文件如下:
// 通过 namespace 创建一个名为 Validation 的命名空间
var Validation;
(function (Validation) {
// 定义一个正则表达式
var isLetterReg = /^[A-Za-z]+$/;
// 这里在定义一个正则表达式,与上一个的区别就是这个正则表达式通过export导出了
Validation.isNumberReg = /^[0-9]+$/;
// 导出一个方法
Validation.checkLetter = function (text) {
return isLetterReg.test(text);
};
})(Validation || (Validation = {}));
/// <reference path='validation.ts' />
var isLetter = Validation.checkLetter('text');
var reg = Validation.isNumberReg;
console.log(isLetter);
console.log(reg);
31. 拆分为多个文件
随着我们的开发内容的不断增加,我们可以将同一个命名命名空间拆分为多个文件分开维护,尽管我们将其拆分为为多个文件,但是他们仍然属于一个命名空间,示例代码如下:
LetterValidation.ts
// LetterValidation.ts
namespace Validation {
export const isLetterReg = /^[A-Za-z]+$/
export const checkLetter = (text: any) => {
return isLetterReg.test(text)
}
}
NumberValidation.ts
// NumberValidation.ts
namespace Validation {
export const isNumberReg = /^[0-9]+$/
export const checkNumber = (text: any) => {
return isNumberReg.test(text)
}
}
index.ts
// index.ts
/// <reference path="./LetterValidation.ts"/>
/// <reference path="./NumberValidation.ts"/>
let isLetter = Validation.checkLetter('text')
const reg = Validation.isNumberReg
console.log(isLetter)
我们使用命令行来编译一下:
tsc --outFile src/index.js index.ts
最终编译后的index.js
代码如下:
// LetterValidation.ts
var Validation;
(function (Validation) {
Validation.isLetterReg = /^[A-Za-z]+$/;
Validation.checkLetter = function (text) {
return Validation.isLetterReg.test(text);
};
})(Validation || (Validation = {}));
// NumberValidation.ts
var Validation;
(function (Validation) {
Validation.isNumberReg = /^[0-9]+$/;
Validation.checkNumber = function (text) {
return Validation.isNumberReg.test(text);
};
})(Validation || (Validation = {}));
/// <reference path="./LetterValidation.ts"/>
/// <reference path="./NumberValidation.ts"/>
var isLetter = Validation.checkLetter('text');
var reg = Validation.isNumberReg;
console.log(isLetter);
由编译结果可以看出,我们先引入了LetterValidation.ts
文件,后引入NumberValidation.ts
文件,他们最终编译后的结果也是按照引入顺序编译的。
32. 命名空间别名
别名是一种简化命名空间的操作方式,其语法是使用import
关键字,使用方式如下:
import q = x.y.z
值得注意的是该方式不要与家长模块的import x = require('name')
语法混淆,这里的语法是为指定的符号创建一个别名。可以使用该方法为任意标识符创建别名,也包括引入模块中的对象。
// 定义一个命名空间
namespace Shapes {
// 在命名空间中定义一个子命名空间,并将其导出
export namespace Polygons {
export class Triangle {}
export class Square {}
}
}
// 通过 import 的语法将导出的子命名空间重新命名为 polygons
import polygons = Shapes.Polygons
// 通过导出的命名空间实例化 Square 类
let sq = new polygons.Square()
通过这个例子我们可以看到,使用import
关键字来定义命名空间中某个输出元素的别名,可以减少我们深层次获取属性的成本。
七、模块
TypeScript中的模块系统是遵循ES6标准的,如果熟悉ES6的模块,在学习TypeScript中的模块时会显得格外的容易。
33. export
和import
在TypeScript中依然使用export
来导出声明,导出的声明不仅有变量、函数、类,还包括TypeScript特有的别名和接口。
关于导出的示例代码如下所示:
// export.ts
// 导出接口
export interface Person {
name: String
}
// 导出类
export class MyClass {
constructor(public person: Person) {}
}
let myClass = new MyClass({ name: '一碗周' })
// 导出对象 通过 as 换一个起一个别名
export { myClass as person }
在上面的代码中,我们不仅可以通过export
来导出接口、类等;而且可以通过as
关键字,将导出的名字更换一个进行导出。
关于导入的代码如下所示:
// import.ts
import { Person } from './export' // 引入接口
// const myName: Person = '一碗周' // error 不能将类型“string”分配给类型“Person”。
// 导入类
import { MyClass } from './export'
// 根据导入的类实例化对象
const person = new MyClass({ name: '一碗周' })
console.log(person) // MyClass { person: { name: '一碗周' } }
// 导入对象 通过 as 换一个名称
import { person as myClass } from './export'
console.log(myClass) // MyClass { person: { name: '一碗周' } }
这里我们引用模块文件的时候省略了 .ts
后缀,这就涉及到一个模块解析策略。
我们拿上面代码中的export
(这里文件名,并不是关键字) 为例,编译器在解析模块引用的时候,如果遇到省略后缀的情况,会依次查找以该名称为文件名的.ts
、.tsx
、.d.ts
文件;如果没找到,会在当前文件夹下的package.json
文件里查找types
字段指定的模块路径,然后通过这个路径去查找模块;如果没找到package.json
文件或者types
字段,则会将 export
当做文件夹去查找,如果它确实是文件夹,将会在这个文件夹下依次查找index.ts
、index.tsx
、index.d.ts
。 如果还没找到,会在上面例子中 export
文件夹的上级文件夹继续查找,查找规则和前面这些顺序一致。
我们可以将我们需要的声明通过 export default
关键字导出,引入时 import
语句不需要大括号,可以为他随便写一个默认的名字 。
示例代码如下所示:
// export-default
interface Person {
name: String
}
class MyClass {
constructor(public person: Person) {}
}
let myClass = new MyClass({ name: '一碗周' })
export default { MyClass, myClass }
// import.ts
import test from './export-default'
console.log(test.MyClass) // [Function: MyClass]
console.log(test.myClass) // MyClass { person: { name: '一碗周' } }
34. export =
和 import = require()
TypeScript可以将代码编译为CommonJS、AMD或其它模块系统代码,同时会生成对应的声明文件。我们知道CommonJS和AMD两种模块系统语法是不兼容的,所以TypeScript为了兼容这两种语法,使得我们编译后的声明文件同时支持这两种模块系统,增加了export =
和import xx = require()
两个语句。
当我们想要导出一个模块时,可以使用export =
来导出:
// export=.ts
interface Person {
name: String
}
class MyClass {
constructor(public person: Person) {}
}
let myClass = new MyClass({ name: '一碗周' })
export = { MyClass, myClass }
然后使用这个形式导出的模块,必须使用import xx = require()
来引入:
// import = require()
import test = require('./export=')
console.log(test.MyClass) // [Function: MyClass]
console.log(test.myClass) // MyClass { person: { name: '一碗周' } }
如果你的模块不需要同时支持这两种模块系统,可以不使用export =
来导出内容。
写在最后
以上就是吐血总结的TypeScript的内容,这篇文章可以用作一个参考。如果我们的文章对你有帮助,可以为我点一个小小赞,谢谢~。
往期推荐