TypeScript入门基础知识 真香~

779 阅读33分钟

前言

看到身边的朋友在工作中都用上了TypeScript,都直呼真香!而我从学习和做TypeScript的项目到现在也有几个月了,也做了一些总结。本文和你们一起分享我的总结!

TypeScript编译器

通常情况下,编译器拿到一段代码后,会转换成抽象句法树(AST)。然后把AST转成字节码。字节码再传给运行时程序计算。最终得到结果。步骤如下:

  1. 把程序解析成AST
  2. AST编译成字节码。
  3. 运行时计算字节码。 TS并不是直接编译成字节码,而是先编译成JS代码。然后像以上步骤那样,在浏览器或node运行得到JS代码。

说到这,可能有些同学就问了。TS是怎么保证代码安全的呢?

至关重要的一个步骤就是:TS编译器生成ATS后,在真正运行代码之前,TS会对代码做类型检查。TS的编译过程大致如下图。

1.png

TSC把TS编译成JS代码时,是不会考虑类型的。类型只在类型检查这步使用。

类型系统

类型系统分两种。

  1. 通过显式句法告诉编译器所有值的类型。
  2. 自动推导值的类型。 这两种类型系统TS都具备,可以显式注解类型,也可以让TS推导多数类型。
// 显式注解类型
let num: number = 1;
let isShow: boolean = false;
let name: string = "图图";

// 自动推导值的类型
let n = 2;  // n是一个数字
let b = true; // b是一个布尔值
let m = ["美美", "牛爷爷"]; // m是一个字符串数组

TypeScript类型大全

下面列出一个TS中的类型结构图。

未命名绘图.jpg

any

any是个兜底类型(表示所有类型),声明any类型的变量后,你可以对它做任何的操作。就跟平时写的JS代码没区别。

let a: any = 1
a = false
a = '图图'
a = [1, 2, 3, 4]
a = a.join('')
a = a.split('')

如果使用any类型,类型检查器就发挥不了其作用,导致运行时抛出错误。我们要尽量避免使用any类型。

unknown

unknown类型是为了解决any类型的缺陷,unknown也可以表示所有值。当你不知道一个值的类型时,又不使用any的情况下,可以使用unknown类型。但TS会要求你做二次检查。

let num: unknown = 1 // unknown
let isNum = num === 2 // boolean
let addNum = num + 100 // Error: Object is of type 'unknown'

// 检查num的类型是什么
if (typeof num === 'number') {
  let a = num + 100 // number
  console.log(d) // 101
}

以上示例中,unknown类型的值可以做比较(isNum)。但是不能直接性对值进行算术运算操作(addNum)。要先检查这个值的确是某个类型(a),上面使用了typeof运算符对num变量的值进行二次确认。除了typeof运算符,还可以用instance运算符。

boolean

boolean类型只有两个值:truefalse

let isShow = true
let isHide: boolean = false

number

number类型包括所有数字。

let num: number = 1
let addNum = 2

bigint

bigint类型是JS原有的类型。处理比较大的数时才用到。

let big1 = 1234n
let big2: bigint = 5678n

string

let name = '图图' // string
let sex: string = '男' // string

symbol

symbol类型是ES6加入的类型。在实际开发中,不太常用。没学ES6的同学,建议看阮一峰老师的ES6书籍。

let names: symbol = Symbol('美美')
let height = Symbol('180')
const weight: unique symbol = Symbol('55') // typeof weight
let g: unique symbol = Symbol('55') // A variable whose type is a 'unique symbol' type must be 'const'

TypeScript为symbol进行了补充,加入了unique symbol类型。在定义这种类型的值时,只能用const而不是let。在Vs Code编辑器中会显示typeof VariableName,而不是unique symbol

null和undefined

在Typescript中,nullundefined也有各自的类型。类型名称也是nullundefined

这两个类型比较特殊,在TypeScript中,undefined类型只有undefined一个值,null类型只有null一个值。

let a: undefined = undefined
let b: null = null

never和void

除了undefinednull以外,还有nevervoid类型。这两个类型都有着明确的作用。

  • void是函数没有显式返回任何值时的返回类型。
  • never是函数根本不返回时使用的类型。(比如函数执行过程中报错了,或者永远运行下去)
// 返回void的函数
function addNum(): void {
  let a = 1 + 1
  let b = a * a
}

// 返回never的函数
function a() {
  throw TypeError('总是报错')
}

// 返回never的函数
function b() {
  while (true) {
    console.log('我在无限循环')
  }
}

如果说unknown是其他每个类型的父类型,那never就是其他每个类型的子类型。可以把never理解成 “兜底类型”。这就说明,never类型可赋值给其他任何类型,在任何地方都可以放心使用never类型的值。

需要注意的是,在使用never类型时。也要像unknown类型那样,对其进行二次检查。

元组

元组array的子类型,用于定义数组的一种特殊方式,长度固定,索引位置上的值具有固定的已知类型。声明元组时必须显式注解类型。因为创建元组用的句法和数组一样,都是方括号。TS遇到方括号,都会认为是数组类型。

let nums: [number] = [1]
let personInfo: [string, number, string] = ['图图', 24, '1998']

元组也支持可选元素剩余元素

// 火车票价数组,不同的路程价格不同
let nums: [number, number?][] = [[100], [200, 259], [300]]

// 字符串列表,至少有一个元素
let names: [string, ...string[]] = ["图图", "小美", "牛爷爷", "图爸爸"]

// 元素类型不同的列表
let list: [number, boolean, ...Object[]] = [1, true, { type: "Object" }, { name: "图图" }]

枚举

枚举是一种无序数据结构,用于列举该类型中的所有值。把key(键)映射到value(值)中。枚举有两种形式:字符串跟字符串之间的映射和字符串跟数字之间的映射。

enum Fruits {
  Apple,
  Banana,
  Orange
}

console.log(Fruits.Apple) // Apple
console.log(Fruits['Orange']) // 2

访问枚举的值有两种方式,方括号[]点号.都可以。不同的方式访问,所得到的值也不同。

TS还会自动推算枚举中每个成员对应的数字,也可以自己手动设置。

enum Car {
  Audi = 1,
  Honda = 2,
  ToYoTa = 3
}

console.log(Car[1]) // Audi
console.log(Car.ToYoTa) // 3

一个枚举可以分几次声明,TS会自动将每一部分合并在一起。

enum Fruits {
  Apple = 0,
  Banana = 1,
  Orange = 2
}

enum Fruits {
  Watermelon = 3
}
console.log(Fruits)
// ts-node在控制台输出的结果
// {
//   '0': 'Apple',
//   '1': 'Banana',
//   '2': 'Orange',
//   '3': 'Watermelon',
//   Apple: 0,
//   Banana: 1,
//   Orange: 2,
//   Watermelon: 3
// }

但要注意的是,分开声明的话TS只会推导其中一部分的值。最好给枚举的每个键都显式赋值。如果把上面的Watermelon的值去掉,它的值就为0

键的值可以通过计算得出,而且不必给所有的键都赋值。TS会推导出缺失的值。

enum Fruits {
  Apple = 10,
  Banana = 10 + 1,
  Orange
}

console.log(Fruits)
{
  '10': 'Apple',
  '11': 'Banana',
  '12': 'Orange',
  Apple: 10,
  Banana: 11,
  Orange: 12
}

上面代码中,Orange键没有赋值。TS自动推导出Orange的值为12,它的前一个键的值为11。这种行为只能是值为数字类型的情况下才会出现。

数组

let nums = [1, 2, 3]

let names: string[] = ['图图', '牛爷爷', '图妈妈']

// 泛型语法
let fruits: Array<string> = ['apple', 'banana', 'orange']

对象

TS中的对象类型表示对象的结构。JS一般采用结构化类型,TS直接沿用。

结构化类型:只关心对象有哪些属性,而不管属性使用什么名称。也叫做鸭子类型(即不以貌取人)。

TS中声明对象类型有几种方式。

object

第一种,把一个值声明为object类型:

let a: object = {
  b: '1111'
}

console.log(a.b) 
// Error Property 'b' does not exist on type 'object'

这种方式声明一个对象,只能表示该值是个对象。而做不了任何操作。

对象字面量

第二种,让TS推导该对象的结构。也就是对象字面量句法。

let person = {
  name: '图图',
  age: 24
}
// ts推导出的类型
// person: {
//   name: string;
//   age: number;
// }

也可以在花括号内明确描述。

let person: { name: string, age: number } = {
  name: '小美',
  age: 18
}
// ts推导出的类型
// person: {
//   name: string;
//   age: number;
// }

对象字面量句法的意思是:这个东西的结构是这样的。

{ name: string, age: number }描述的是一个对象的结构,上面的例子中的对象字面量满足了该结构,如果添加额外的属性或缺少必要的属性时,就会报错。

let person: { name: string, age: number } = {
  name: '图图',
  age: 24
}
person.height = 175
// Error Property 'height' does not exist on type '{ name: string; age: number; }'
// 类型{ name: string; age: number; }上不存在height属性

默认情况下,TS对对象的属性要求非常严格。如果声明对象有个类型为number的属性ageTS将预期对象有这么一个属性,而且也只有这一个属性。如果缺少age属性,或者多了其他属性,就会报错。

针对这种情况,后面会讲到可选属性索引签名

空对象类型

对象字面量表示还有一种:空对象类型({})。除了nullundefined以外的任何类型都可以赋值给空对象类型,用起来比较复杂,建议不要使用这种方式。

let car: {}

car = {}
car = { name: 'audi' }
car = []
car = 'ToYoTa'

Object

最后一种声明对象类型的方式:Object。和{}的作用一样的。不推荐使用。

通常情况下,只推荐前两种方式声明对象类型。如果对对象的字段没有要求,那么就使用第一种。如果想知道对象有哪些字段,或者对象的值都为相同的类型,就使用第二种。

类型字面量

我们先来看一段代码。

let show: true = true
let disable: false = true
// Error Type 'true' is not assignable to type 'false'.

可以看到show的类型并不是普通的boolean类型,而是只为trueboolean类型。这就是类型字面量。而disabele变量的类型为false,但值确是true。此时,TS就报错了。

把类型设为某个值,就限制了变量在所有值中只能取指定的值。这称为类型字面量

类型字面量:表示一个值的类型。

可选属性

TS中可以将对象的属性设为可选属性。用法则是在对象的键和:之间加上一个?符号表示该属性是可选的。

// 可选属性 height
let person: {
  name: string,
  age: number,
  height?: number,
}


person = {
  name: '小美',
  age: 18
}

console.log(person)
// { name: '小美', age: 18 }

person = {
  name: '图图',
  age: 18,
  height: 175
}

console.log(person)
// { name: '图图', age: 18, height: 175 }

这里我们将height设置成了可选的(类型为number | undefined)。传不传都行。

索引签名

当不确定一个对象的类型时或者在未来会对对象添加更多的键时,可以使用索引签名声明对象的类型。索引签名的语法为[key: T]: U,意思是:这个对象里的键类型为T,值则为U类型。

let person: {
  [key: string]: any
} = {
  name: '牛爷爷',
  age: 60,
}

person.height = 160
person.weight = 100
person.sex = '男'

有了索引签名,除了显式声明的键之外,还可以放心的添加更多键。要注意的是,键的类型只能是stringnumber类型。键的名称可以是任何的词。不一定像上面那样用key

类型别名

类型别名用于给类型声明一个新名。

type Height = number
type Person = {
  name: string,
  height: Height
}

let person: Person = {
  name: '图爸爸',
  height: 180
}

类型别名和letconst变量声明一样,同一类型不能声明两次。并且也是采用块级作用域。每个代码块和每个函数都有自己的作用域,内部的类型别名会覆盖外部的类型别名。

type Name = '图图'

let n = Math.random() < 0.5
if (n) {
  type Name = '小美' // 覆盖上面声明的Name
  let name: Name = '小美'
  console.log('name=', name)
} else {
  let name: Name = '图图'
  console.log('name=', name)
}

交叉类型

交叉类型就是把多个类型合并在一起。并且具备多个类型的特性。也可以叫做并集类型

type Bad = { name: string, isBad: boolean }
type Good = { name: string, isGood: boolean, clever: boolean }

type BadAndGood = Bad & Good

let person: BadAndGood = {
  name: '蟑螂恶霸',
  isBad: true,
  isGood: false,
  clever: false
}

上面的代码中,声明了两个不同的类型BadGood。然后使用&运算符声明了BadGood两者之和的交叉类型BadAndGood。该类型具备BadGood这两种类型的属性。

联合类型

当一个变量存在不同的类型时,联合类型就派上用场了。

let height: number | string = 175

height = '180'
height = 190

height的值的类型可以是string类型,也可以是number类型。

函数

函数声明

JSTS函数声明方式一共有五种。

// 具名函数
function Person1(name: string): string {
  return `hello ${name}`
}

// 函数表达式
let Person2 = function (name: string): string {
  return `hello ${name}`
}

// 箭头函数表达式
let person3 = (name: string): string => {
  return `hello ${name}`
}

// 箭头函数表达式简写
let person4 = (name: string): string => `hello ${name}`

// 函数构造方法
let person5 = new Function('name', 'return `hello ${name}`')

形参和实参

我们来简单回顾一下形参实参

  • 形参:声明函数时指定的运行函数所需的数据
  • 实参:调用函数时传给函数的数据

可选参数和默认参数

TS的函数也是可以用?将参数设为可选的。定义参数时最好是把必要的参数放在前,可选放在后。

function person(name: string, age?: number) {
  return `大家好,我是${name}。今年${age || 18}岁`
}

console.log(person('图图', 20))
console.log(person('小美'))

JS中的函数参数默认值,在TS中一样支持的。把上面的函数改一下。

function person(name: string, age: number = 18) {
  return `大家好,我是${name}。今年${age || 18}岁`
}

console.log(person('图图', 20))
console.log(person('小美'))

这个例子中,我们把可选参数age改成了默认值。调用函数时,可以不传。

剩余参数

TS中的剩余参数和ES6中的剩余参数是一样的。以三点运算符(...)表示。

function sum(...nums: number[]): number {
  return nums.reduce((total, n) => total + n, 0)
}

console.log(sum(1, 2, 3)) // 6

注解this的类型

TS中,如果函数用到this,就要在函数的第一个参数中声明this的类型(放在其他参数之前),这样每次调用函数时,确保this是你想要的类型。

function getDate(this: Date) {
  return `${this.getFullYear()}-${this.getMonth()}-${this.getDate()}`
}

let day = getDate.call(new Date)

console.log(day)
// 2022-4-14

想了解更多关于TS中的this,可以到TS的官方文档查看。

函数签名

在学习函数签名之前,先来给一个例子大家看:

function sum(a: number, b: number): number {
  return a * b;
}

这个例子中的sum是一个函数,它的类型是Function。但有时候Function类型并不是我们想要的。它并不能体现出函数的具体类型。

那么,sum函数的类型要怎么表示呢?sum是一个接受两个number参数并返回一个number的函数。在TS中可以像下面这样来表示该函数的类型:

(a: number, b: number) => number

这个句法在TS表示函数的类型,也叫函数签名(或叫类型签名)。它跟箭头函数非常相似。

函数签名只包含类型层面的代码。也就是说,只有类型没有值。因此,函数签名可以表示参数的类型、this的类型、返回值的类型、剩余参数的类型和可选参数的类型。但无法表示默认值,因为默认值是值,而不是类型。函数签名没有函数的定义体,无法推导出返回类型,所以必须显式注解。

下面我们来看看函数签名的使用。

type Sum = (a: number, b: number) => number

let sum: Sum = (a, b = 30) => {
  return a + b
}

console.log(sum(10, 20))

这个例子中,声明一个函数表达式sum,并注解它的类型为Sum。参数的类型不用再次注解,因为在定义Sum类型时已经注解过了。给b设置一个默认值。类型则从Sum函数签名中推导出,但默认值是不知道的,因为Sum是类型,不包含值。最后不许再次注解返回类型,在Sum函数签名中已经声明为number

类型层面和值层面代码

类型层面的代码指的是只有类型和类型运算符的代码。其他都是值层面代码。看下面的例子。

// 函数的参数、返回值类型、并集类型运算符|都是类型层面
function add(num: number): number | null {
  if (num < 0) {
    return null;
  }
  num++;
  return num;
}

let num: number = 1; // 类型层面
let total = add(num);
if (total !== null) {
  console.log(total);
}

这个例子中,函数的参数、返回值类型、联合类型运算符|都是类型层面。

函数签名两种句法

函数签名句法有两种。上面用是简写型函数签名。还有一种是完整行函数签名

// 简写型函数签名
type Sum = (a: number, b: number): number

// 完整型函数签名
type Sum = {
  (a: number, b: number): number,
}

这两种写法完全相同,只是使用句法不同。对于比较复杂的函数时,推荐用完整型函数签名句法。也就是下面讲到的函数重载

函数重载

函数重载:具有多个函数签名的函数。

我们都知道在JS中是没有函数重载的,但在TS中有。下面我们通过函数签名来实现一个以不同的方式调用的函数。

type Sum = {
  (a: number, b: number): number,
  (a: number, b: number, c:number): number
}

let sum: Sum = (a:number, b:number, c?:number) => {
  if (c !== undefined) {
    return a + b +c
  }
  return a + b
}

console.log(sum(2, 3)) // 5

console.log(sum(2, 3, 5)) // 10

这个例子中,我们写两个函数签名:一个接受两个参数,另一个接受三个参数。根据传入的参数不同,函数体内所做的事情就不同。

声明函数重载还有另一种方式,我们来改造一下上面的例子。

function Sum(a: number, b: number): number
function Sum(a: number, b: number, c: number): number
function Sum(a: number, b: number, c?: number) {
  if (c !== undefined) {
    return a + b + c
  }
  return a + b
}

泛型

在类型层面施加约束的占位类型,也叫多态类型参数

了解泛型之前,我们先来看个例子。

function merge(arr1: string[], arr2: string[]): string[];
function merge(arr1: number[], arr2: number[]): number[];
function merge(arr1: object[], arr2: object[]): object[];
function merge(arr1: any, arr2: any) {
  return arr1.concat(arr2)
}

const strings = merge(['a', 'b'], ['c', 'd', 'e'])
const numbers = merge([1, 2, 3], [4, 5])
const objs = merge([{ name: '图图' }], [{ name: '小美' }])

console.log(strings[0])
console.log(numbers[0])
console.log(objs[0].name)
// Error Property 'name' does not exist on type 'object'.

这个例子中,简单的使用函数重载实现了一个合并字符串数组、数字数组、对象数组的merge函数。访问stringsnumbers数组的第一个元素都没问题。但是当访问objs变量第一个元素的属性时,TS抛出了错误。是因为object无法描述对象的结构,所以抛出了错误。而且没有指明对象的具体结构。

为了解决这种问题,我们就要用到泛型了。把上面的函数接受的参数改写为泛型。如下:

function merge<T>(arr1: T[], arr2: T[]): T[];

function merge<T>(arr1: T[], arr2: T[]) {
  return arr1.concat(arr2)
}

const strings = merge(['a', 'b'], ['c', 'd', 'e'])
const numbers = merge([1, 2, 3], [4, 5])
const objs = merge([{ name: '图图' }], [{ name: '小美' }])

console.log(strings[0]) // a
console.log(numbers[0]) // 1
console.log(objs[0].name) // 图图

上面代码中,merge函数使用一个泛型参数T,但我们并不知道具体类型是什么。TS从传入的arr1arr2中推导T的类型。调用merge函数时,TS推导出T的具体类型之后,会把T出现的每个地方都替换成推导出的类型。T就像是一个占位类型,类型检查器会根据上下文填充具体的类型。

泛型使用尖括号<>来声明(你可以把尖括号理解成type关键字,只不过声明的是泛型)。尖括号的位置限定泛型的作用域(只有少数几个地方可以用尖括号),TS将确保当前作用域中相同的泛型参数最终都绑定同一个具体类型。鉴于上面的例子中括号的位置,TS将在调用merge函数时为泛型T绑定具体类型。而为T绑定哪一个具体类型,就取决于调用merge函数时传入的参数。

T是一个类型名称,也可以使用任何名称,比如NamePersonValue等。

多个泛型

泛型还可以是多个,在尖括号里以逗号分隔开。

function getPerson<T, U>(name: T, age: U) {
  return {
    name, age
  };
}

const person = getPerson("图图", 18);
console.log(person) // { name: '图图', age: 18 }

上面代码中,有两个泛型:表示人名的T和年龄的U,最后返回一个具备这两个值的对象。

绑定泛型

声明泛型的位置不仅限制了泛型的作用域。还决定TS什么时候给泛型绑定具体类型。

type Person = {
  <T, U>(name: T, age: U): {}
}
let person: Person = (x, y) => {
  return {
    x, y
  }
}

<T, U>在函数签名中声明,TS会在调用Person类型的函数时为TU绑定具体的类型。

如果把<T, U>的作用域限制在函数签名Person中,TS会要求在使用Perosn时显示绑定类型。

type Person<T, U> = {
  (name: T, age: U): {}
}
​
// 错误例子
let person: Person = (name, age) => {
  return { name, age }
}
// Error Generic type 'Person' requires 2 type argument(s)type OtherPerson = Person
// Error Generic type 'Person' requires 2 type argument(s)// 正确例子
let person: Person<string, number> = (name, age) => {
  return { name, age }
}

type OtherPerson = Person<string, number>

TS在使用泛型时会给泛型绑定具体类型,对于函数来说,在调用函数时。对于类,在实例化时。对于函数签名和接口,在使用别名和实现接口时。

泛型推导

以上的所有泛型例子,都是让TS自动推导出泛型。不过,也可以显示注解泛型。在显式注解泛型时。要么把所有的泛型都加上,要么都不注解。

function Person<T, U>(name: T, age: U): {} {
  return { name, age };
}
​
console.log(Person('图图', 23)) // ok
console.log(Person<string, number>('小美', 18)) // okconsole.log(Person<string>('牛爷爷', 60))
// Error Expected 2 type arguments, but got 1console.log(Person<string, string>('图爸爸', 49))
// Error Argument of type 'number' is not assignable to parameter of type 'string'

泛型别名

type关键字可以给类型起新名字,泛型同样也可以。下面来定义一个Event类型,用于描述DOM事件。

// 类型别名只有在这个地方可以声明泛型
type DomEvent<T> = {
  target: T,
  type: string
}
​
// 泛型别名
type DivEvent = DomEvent<HTMLDivElement | null>;
​
// 第一种
let myDiv: DivEvent = {
  target: document.querySelector('#my-div'),
  type: 'click'
}
​
// 第二种 显示注解类型参数
let myDiv: DomEvent<HTMLDivElement | null> = {
  target: document.querySelector('#my-div'),
  type: 'click',
}

DomEventtarget属性指向触发事件的元素,比如一个div或者button等。要注意的是,在使用DomEvent泛型时。必须显式注解类型参数,因为TS无法推导。能用泛型的地方,泛型别名一样生效。

泛型默认类型

函数的参数可以指定默认值,泛型参数也可以指定默认类型。

type DomEvent<T = HTMLElement> = {
  target: T,
  type: string
}
​
let myButton: DomEvent<HTMLButtonElement | null> = {
  target: document.querySelector('#my-btn'),
  type: 'click'
}

注意,泛型默认类型和函数可选参数一样的,有默认类型的泛型要放在没有默认类型的泛型后面。

// Error Required type parameters may not follow optional type parameters
type MyEvent<T = HTMLElement, Type> = {
  target: T,
  type: Type
}
​
let myDiv: MyEvent<HTMLElement | null, string> = {
  target: document.querySelector('#my-div'),
  type: 'scroll'
}

下面我们来看一个类的例子。

class Fruit {
  fruitName: string
  constructor(name: string) {
    this.fruitName = name
  }

  getFruitName() {
    return this.fruitName
  }
}

let person = new Fruit('苹果');

这里声明了一个Fruit类。该类有三个成员:一个fruitName属性、一个构造函数和一个getFruitName方法。引用类中的成员时用了this,它表示我们访问类中的成员。

最后,使用new运算符创建了Fruit类的实例,它会调用之前给定的构造函数,创建一个Fruit类型的新对象,并执行构造函数初始化它。

继承

和JS一样,通过extends关键字实现子类继承父类的属性和方法。

class Fruit {
  fruitName: string
  constructor(name: string) {
    this.fruitName = name
  }

  getFruitName() {
    return this.fruitName
  }
}

class Grape extends Fruit {
  fruitName: string
  price: number
  constructor(name: string, price: number) {
    super(name);
    this.fruitName = name
    this.price = price
  }
}
const grape = new Grape('葡萄', 15)

console.log(grape.getFruitName()) // 葡萄

这个例子中,Grape子类继承了Fruit父类上的属性和方法。在创建Grape的实例后,通过子类调用父类中的方法。

注意,子类有一个构造函数。在构造函数中必须调用super(),把父子关系连接起来。并且要在构造函数访问this的属性之前调用。

类中的修饰符

类中的属性和方法支持以下三个访问修饰符:

  • public公有的,任何地方都可以访问
  • protected受保护的,只能在当前类及其子类中访问
  • private私有的,只能在当前类访问

public

在TS中,类的成员默认为公有的。你也可以把标记成员为public

class Fruit {
  public fruitName: string
  constructor(name: string) {
    this.fruitName = name
  }

  public getFruitName() {
    return this.fruitName
  }
}

const fruit = new Fruit('西瓜')
console.log(fruit.getFruitName()) // 西瓜

private

如果把成员标记为private后,它只能在类中访问。

class Fruit {
  public fruitName: string
  private price: number
  constructor(name: string, price: number) {
    this.fruitName = name
    this.price = price
  }

  public getFruitName() {
    return this.fruitName
  }
}

const fruit = new Fruit('猕猴桃', 50)
console.log(fruit.price)
// Error Property 'price' is private and only accessible within class 'Fruit'

protected

把成员标记为protected后,在子类中可以访问,但不能在父类或子类外访问。

class Fruit {
  public fruitName: string
  protected price: number
  constructor(name: string, price: number) {
    this.fruitName = name
    this.price = price
  }

  public getFruitName() {
    return this.fruitName
  }
}

class Watermelon extends Fruit {
  constructor(name: string, price: number) {
    super(name, price);
    this.fruitName = name
    this.price = price
  }

  public getPrice() {
    return this.price
  }
}

const watermelon = new Watermelon('西瓜', 3)
console.log(watermelon.getPrice()) // 3
console.log(watermelon.price)
// Error Property 'price' is protected and only accessible within class 'Fruit' and its subclasses

readonly

声明实例属性时可以使用readonly修饰符把属性标记为只读。readonly修饰符不只是可以在类中使用,还可以把数组和对象属性设为只读。

class Fruit {
  readonly fruitName: string
  readonly price: number
  constructor(name: string, price: number) {
    this.fruitName = name
    this.price = price
  }
}

const fruit = new Fruit('火龙果', 10)
fruit.price = 20
fruit.fruitName = '荔枝'
// Cannot assign to 'fruitName' because it is a read-only property

抽象类

抽象类是作为其它子类的父类使用,它不能被实例化。抽象类可以包含成员的实现细节。使用abstract关键字来定义抽象类、抽象方法。

abstract class Fruit {
  fruitName: string
  price: number
  constructor(name: string, price: number) {
    this.fruitName = name
    this.price = price
  }

  abstract getPrice(): number
  abstract getName(): string
}

const fruit = new Fruit('龙眼', 15)
// Error Cannot create an instance of an abstract class

这个例子中,用abstract关键字定义了Fruit类,直接实例化Fruit后,TS直接报错了。

下面我们通过子类去实现抽象类中定义的方法。

abstract class Fruit {
  fruitName: string
  price: number
  constructor(name: string, price: number) {
    this.fruitName = name
    this.price = price
  }

  abstract getPrice(): number
  abstract getName(): string
}

class Watermelon extends Fruit {
  constructor(name: string, price: number) {
    super(name, price)
    this.fruitName = name
    this.price = price
  }

  getPrice(): number {
    return this.price
  }

  getName(): string {
    return this.fruitName
  }
}

const watermelon = new Watermelon('西瓜', 6)
console.log(watermelon.getPrice()) // 6
console.log(watermelon.getName()) // 西瓜

这里的Watermelon类继承了Fruit抽象类,并且实现了Fruit类上定义的getPricegetName方法。如果忘记实现其中一个方法,TS就会抛出错误。

抽象类中的抽象方法是不会具体实现的,而是交给子类实现。抽象类必须要有一个抽象方法,继承抽象类的子类必须重写抽象方法。也就是说抽象类负责定义,子类负责实现。

接口

和类型别名类似,接口是一种命名类型的方式。类型别名和接口算得上是同一个概念的两种句法,就跟函数表达式和函数声明之间的关系。但两者之间还是会存在一些差别的。先来看看二者的共同点:

type Fruits = {
  name: string,
  price: number,
  feature: string,
  weight: number,
}

// 使用接口重新定义上面的类型别名
interface Fruits {
  name: string,
  price: number,
  feature: string,
  weight: number,
}

在使用Fruits类型别名的地方都能用Fruits接口。两者都是定义结构。

还可以把类型组合在一起。

type Fruits = {
  price: number,
}

type Watermelon = Fruits & {
  feature: string,
}

type Banana = Fruits & {
  weight: string,
}

// 使用接口重新定义上面的类型别名
interface Fruits {
  price: number,
}

interface Watermelon extends Fruits {
  feature: string,
}

interface Banana extends Fruits {
  weight: string
}

接口不一定扩展其他接口。接口可以扩展任何结构:对象类型、类或其他接口。

接口和类型别名的差异

类型别名和接口之间有三种差别。

  1. 类型别名更通用,右边可以是任何类型,包括类型表达式(类型外加&|运算符);而在接口声明中,右边只能是结构。看下面的例子。
type Str = string
type StrAndNum = Str | number
  1. 扩展接口时,TS会检查扩展的接口是否可赋值给被扩展接口
interface Person {
  good(x: number): string,
  bad(x: number): string,
}

interface BadPerson extends Person {
  good(x: number | string): number
  bad(x: string): string // Error
}

// 替换成类型别名,不会抛出错误
type Person = {
  good(x: number): string,
  bad(x: number): string,
}

type BadPerson = Person & {
  good(x: number | string): number
  bad(x: string): string
} 

这个例子中,将接口换成类型别名,把extends换成交集运算符&,TS会把扩展和被扩展的类型组合在一起,最后的结果就是重载bad的签名。

  1. 同一个作用域中的多个同名接口会自动合并;同一个作用域中的多个类型别名会导致编译时报错。这个特性叫做声明合并

实现

implements(实现)关键字的作用是指明该类满足某个接口。和其他显示类型注解一样。这是给类添加类型层面约束的一种方式。这样做的目的是尽可能的保证类在实现上的正确性。看下面的例子。

interface Fruit {
  getName(): string
  setName(name: string): void
  setPrice(price: number): void
}

class Banana implements Fruit {
  name: string
  price: number
  constructor(name: string, price: number) {
    this.name = name
    this.price = price
  }
  getName(): string {
    return this.name
  }

  setName(name: string): void {
    this.name = name
  }

  setPrice(price: number): void {
    this.price = price
  }
}

const fruit = new Banana('香蕉', 5)

console.log(fruit.getName()) // 香蕉
fruit.setPrice(10)
console.log(fruit.getPrice()) // 10

这个例子中,Banana类必须实现Fruit接口声明的所有方法。如果有需要,还可以在此基础上实现其他方法和属性。接口还可以声明实例属性,但不能带有可见性修饰符(privatepublic等等),也不能使用static关键字。但是可以使用readonly将实例属性标记为只读。

interface Fruit {
  readonly name: string
  getName(): string
  setName(name: string): void
  setPrice(price: number): void
}

一个类不限于只能实现一个接口,你想实现多少个都行。

interface Fruit {
  name: string
  price: number
  getName(): string
}

interface FruitFeature {
  getPrice(): number
}

class Banana implements Fruit, FruitFeature {
  name: string
  price: number
  constructor(name: string, price: number) {
    this.name = name
    this.price = price
  }

  getName(): string {
    return this.name
  }

  getPrice(): number {
    return this.price
  }
}

const fruit = new Banana('香蕉', 6)

console.log(fruit.getName()) // 香蕉
console.log(fruit.getPrice()) // 6

如果忘记实现某个方法或属性,或者实现方式有问题,TS将会抛出错误。

类是结构化类型

和其他类型一样,TS会根据结构比较类,和类的名称没有关系。类跟其他类型是否兼容,要看结构。如果一个对象定义了同样的属性或者方法,也和类兼容。

class Person {
  name: string
  constructor(name: string) {
    this.name = name
  }
  getName(): string {
    return this.name
  }
}

class OtherPerson {
  name: string
  constructor(name: string) {
    this.name = name
  }
  getName(): string {
    return this.name
  }
}


function getPersonInfo(person: Person) {
  return person.getName()
}


let person = new Person('图图')
let otherPerson = new OtherPerson('小美')

getPersonInfo(person) // ok
getPersonInfo(otherPerson) // ok

上面代码中,getPersonInfo函数接收一个person实例。当我们传入person实例和otherPerson实例时。TS并没有报错。在person函数看来,这两个类是可互用的,这两个类都实现了getName方法。如果把方法用到privateprotected修饰符,情况就不一样了。

TypeScript采用的是结构化类型。对类来说,和一个类结构相同的类型是可以互相赋值的。

类型断言

类型断言用作于手动指定一个值的类型。有两种句法。as句法和尖括号<>句法。下面来展示这两种句法的用法。

interface Fruit {
  name: string,
  price: number
}

let fruit: Fruit = {
  name: '猕猴桃',
  price: 30
}

function getFruitPrice(price: number): string | number {
  return price
}

getFruitPrice(fruit.price as number)

上面的例子中,用类型断言as告诉TS,price是个数字,而不是string | number类型。

非空断言

在TypeScript中,通过id获取一个DOM节点,对该节点做某些操作时,通常都要判断该节点是否存在。尽管我们知道一定存在这个元素。但TypeScript只知道该节点的类型为Node | null,所以得用if语句判断。在不确定是否为null的情况下,确实得这么做,但确定不可能是null | undefined时,可以使用TypeScript的非空断言

// 第一种
const dom = document.getElementById('item1')!;
dom.style.display = 'none';

// 第二种
const dom = document.getElementById('item1');
dom!.style.display = 'none';

使用非空断言运算符!,告诉TypeScript,这个变量不可能为null | undefined

明确赋值断言

有这么一种情况,假设我们声明了一个变量userName,但还没赋值就想对其做一些处理。实际上TS并不允许这么操作。但可以使用明确赋值断言告诉TS,userName变量一定有值(注意下面的感叹号)。

// 在没有使用明确赋值断言的情况下
let userName: string

userName.slice(0, 3)
// Error Variable 'userName' is used before being assigned.


// 使用明确赋值断言后报错消失
let userName!: string

userName.slice(0, 3)

const

TS中有个特殊的const类型,可以禁止变量的类型拓宽。const类型用作类型断言。

let fruit = {
  name: '苹果',
  price: 10
} as const

fruit.price = 15
// Error Cannot assign to 'price' because it is a read-only property.

const不仅可以限制类型拓宽,还会把成员设置成readonly。不管数据结构嵌套有多深。

类型进阶

键入运算符

“键入” 运算符类似JavaScript对象查找字段的句法。用于查找对象属性的类型。用 “键入” 查找属性的类型时,只能使用方括号表示法,不能使用点号表示法。看下面的例子。

type APIResponse = {
  status: string,
  message: string,
  result: {
    userName: string,
    userId: string,
    state: number
  }
}

type Result = APIResponse['result']
/*
Result的类型为
{
  userName: string;
  userId: string;
  state: number;
}
*/

type Status = APIResponse['status']
// Status的类型为string

keyof

keyof运算符可以获取对象类型的所有键的类型,并返回一个字符串字面量的联合类型

type Fruit = {
  name: string
  price: number,
  weight: number
}

type FruitKeys = keyof Fruit // name | price | weight

let price: FruitKeys = 'price' // 通过
let weight: FruitKeys = 'weight' // 通过
let fruitName: FruitKeys = 'Name' // Error

有了keyof运算符,搭配 “键入” 运算符可以安全的读取类型。下面我们来实现一个获取对象中指定属性的值函数。

function getProperty<O extends object, K extends keyof O>(obj: O, key: K): O[K] {
  return obj[key]
}

let fruit: Fruit = {
  name: '苹果',
  price: 10,
  weight: 1
}

console.log(getProperty(fruit, 'name')) // 通过
console.log(getProperty(fruit, 'price')) // 通过
console.log(getProperty(fruit, 'Weight'))
// Error Argument of type '"Weight"' is not assignable to parameter of type 'keyof Fruit'

这个例子中,getProperty函数接收一个对象obj和一个键keykeyof O是一个字符串字面量的联合类型,表示obj的所有键。K类型是这个联合类型的子类型。比如,obj的类型是{name: string, price: number, weight: number},那么keyof O的类型就是name | price | weight,而K可以是类型'name'、'price'、'name'|'weight'。接下来,O[K]的类型就是在O中查找K得到的具体类型。如果Kname,那就返回一个字符串。如果K'price' | 'weight',就返回number

Record类型

Record类型用于定义一个对象的键(key)和值的类型(value)。看下面的例子。

interface Options {
  baseUrl: string;
  env: string;
  method: string;
}

// 第一种用法
let options: Record<keyof Options, string> = {
  baseUrl: 'http://www.baidu.com',
  env: 'dev',
  method: 'post',
};

// 第二种用法
let options: Record<string, string> = {
  baseUrl: 'http://www.baidu.com',
  env: 'dev',
  method: 'post',
}

这个例子中,Record有两种用法。

  • 第一种:使用Record类型后,约束了options对象的键必须和Options接口的键一一对应,而值只能是string
  • 第二种:只是要求options对象的键为string类型,并没有特意的限制options对象的键。

映射类型

映射类型是TS独有的语言特性。下面我们来看个简单的例子。

type Keys = 'name' | 'age' | 'height'

type Person = {
  [P in Keys]: number | string
}

let person: Person = {
  name: '小美',
  age: 18,
  height: 165
}

语法和索引签名的语法相同,只不过内部使用了for...in遍历类型。字符串字面量联合类型Keys,包含了要迭代的属性名集合,最后是结果类型number | string

其实,Record类型也是用映射类型实现的:

type Record<K exteds keyof any, V> = {
  [P in K]: V
}

下面来看几个例子,看看使用映射类型都能做哪些事:

type Result = {
  userId: number
  tags: string[],
  personName: string
}

type APIResponse = {
  message: string,
  status: string,
  result: Result
}

// 设置所有键都是可选的
type OptionAPIResponse = {
  [K in keyof APIResponse]?: APIResponse[K]
}
// 设置所有值都可为null
type NullAPIResponse = {
  [K in keyof APIResponse]: APIResponse[K] | null
}
// 设置所有键值只读
type ReadonlyAPIResponse = {
  readonly [K in keyof APIResponse]: APIResponse[K]
}
// 设置所有键值都是可写的 // 等价于APIResponse
type WritableAPIResponse = {
  -readonly [K in keyof APIResponse]: APIResponse[K]
}
// 设置所有字段都是必须的 // 等价于APIResponse
type MustAPIResponse = {
  [K in keyof APIResponse]-?: APIResponse[K]
}

最后两个类型使用了减号-+号运算符。一般情况下不会直接使用+加号运算符,因为它通常蕴含在其他运算符中。在映射类型中,readonly等价于+readonly?等价于+?+存在的意义只是为了确保整体协调。

内置映射类型

Record<Keys, Values>

上面已经提过Record类型,用于指定对象的键和值类型

Partial<Object>

Partial类型用于把对象类型的每个字段都设置为可选的。

interface Person {
  name: string;
  age: number;
  height: number;
}

let person: Partial<Person> = {
  name: '图图',
  height: 180,
};

Required<Object>

Required类型用于把对象类型的每个字段都标记为必须的。

interface Person {
  name?: string;
  age?: number;
  height?: number;
  weight?: number | string;
}

let person: Required<Person> = {
  name: '小美',
  age: 18,
  height: 170,
};
// 少传了一个weight属性,TS就报错了
//Error: Property 'weight' is missing in type '{ name: string; age: number; height: number; }' but required in type 'Required<Person>'

Readonly<Object>

Readonly类型用于把对象类型中的每个字段都设置为只读。

interface ListItem {
  id: number;
  area: string;
  goodsName: string;
  price: number;
}

let goodsInfo: Readonly<ListItem> = {
  id: 1,
  area: '深圳市南山',
  goodsName: '罗技MX3 master3',
  price: 499,
};

goodsInfo.price = 899;
// Error:  Cannot assign to 'price' because it is a read-only property

Pick<O, K>

Pick类型用于从一个对象类型中,选取指定的属性,并返回一个新定义的类型。

interface APIResponse {
  message: string,
  status: string,
  result: string[]
}

type Result = Pick<APIResponse, 'result'>

let res1: Result = {
  result: ['1', '2', '3']
} // ok

let res2: Result = {
  result: ['1', '2', '3'],
  status: 'C0000'
}
// Error: Type '{ result: string[]; status: string; }' is not assignable to type 'Result'.

// Result 等价 ArrayResult
interface ArrayResult {
  notes: string[];
}

Pick类型新建了一个Result类型,和APIResponse建立映射。在使用Result类型时,res2对象中多出了一个status键。TS报错了。因为Result的类型为{ result: string[] }

条件类型

条件类型是TypeScript中比较独特的特性,语法和JavaScript的三元表达式差不多。只不过是用在类型中。一起来看下面的例子。

type IsString<T> = T extends string ? true : false

type X = IsString<string> // true
type Y = IsString<number> // false

这里声明了一个类型IsString,它有一个泛型参数T。条件类型为T extends string,也就是说T是不是为number类型或子类型。如果是,那么得到的类型就为true。否则就是false

条件类型还可以进行嵌套,而且不限于用在类型别名当中,用到类型的地方几乎都可以用。

infer关键字

infer关键字表示在条件类型中待推导的类型。下面来看个例子。

type ElementType<T> = T extends (infer U)[] ? U : T
type Y = ElementType<number[]> // number类型

let y: Y = 1

// 等价
type ElementType2<T> = T extends unknown[] ? T[number] : T
type X = ElementType2<number[]>

在这个例子中,ElementType类型接收泛型参数T。注意,infer子句声明了一个新的类型U,TS会根据传给ElementTypeT推导出U的类型。U是在行内声明的,没有和T一起。

下面我们再来个复杂一点的例子。

type Person<T> = T extends { name: infer P; age: infer P } ? P : T

type Age = Person<{ name: string, age: number }> // string | number
type Name = Person<{ name: string, age: string }> // string


let personName: Name = '小美' // ok
let age: Age = 18 // ok
let weight: Name = 50 // Error

可以看到,Age的类型为string | numberName类型为string。利用这一特性,可以将元组中的类型转成联合类型。

内置条件类型

Exclude<T, U>

Exclude用于计算在T中而不在U中的类型。

type X = number | string
type Name = string

type Y = Exclude<X, Name> // number

Extract<T, U>

Extract用于计算T中可赋值给U的类型。

type X = number | boolean
type Y = string | boolean

type Total = Extract<X, Y> // boolean

NonNullable<T>

NonNullable用于从T中排除nullundefined

type X = null | string | undefined | boolean

type Y = NonNullable<X> // string | boolean

ReturnType<F>

ReturnType用于计算函数的返回类型(不适用于泛型和重载的函数)。

type CallBack = (p: Record<string, unknown>) => string | null

type RType = ReturnType<CallBack> // string | null

InstanceType<C>

InstanceType用于计算类构造方法的实例类型。

type X = { new(): Y }
type Y = { name: string }
type I = InstanceType<X> // { name: string }

命名空间

在开发的过程中,难以避免出现多个同名但含义不同的函数或者变量等等。虽说不建议这样,但总难以避免这些问题发生。而命名空间就能解决你的这些烦恼。

假设有这么两个文件:一个文件是封装请求的模块,另一个是使用该模块发起请求。

// request.ts 这里简单的模拟
exprot namespace Request {
  export function get(url: string): Promise<unknown> {
    return new Promise((resolve) => {
      resolve(`请求url为${url}`)
    })
  }
}
// Home.ts
import { Request } from './request'
async function getList() {
  const res = await Request.get('http://baidu.com')
  console.log(res)
}

getList()

namespace关键词表示命名空间命名空间必须要有名称。可以导出函数、变量、类型、接口或其他命名空间。如果namespace块没有显式导出代码,就表示为所在块的私有代码。

命名空间还可以导出命名空间,因此命名空间可以进行嵌套。比如Request模块增加了其他的请求方法,分成几个子模块。可以改写成命名空间。

// request.ts
export namespace Request {
  export namespace Get {
    export function get(url: string): Promise<unknown> {
      return new Promise((resolve) => {
        resolve(`请求url为${url}`)
      })
    }
  }

  export namespace Post {
    export function post(url: string, data: Record<string, unknown>): Promise<unknown> {
      return new Promise((resolve) => {
        resolve(`请求url为${url}`)
      })
    }
  }
}
// Home.ts
import { Request } from './get'
async function getList() {
  const res = await Request.Get.get('http://baidu.com')
  console.log(res)
}

async function saveForm() {
  const data = {
    userName: '小美',
    password: '123456'
  }
  const res = await Request.Post.post('http://baidu.com', data)
}

getList()
saveForm()

现在我们把Request模块中的请求方法分成了几个子命名空间。不过,命名空间和接口一样。可以声明多个同名的命名空间,TS会递归合并名称相同的命名空间。

export namespace Request {
  export namespace Get {
    export function get(url: string): Promise<unknown> {
      return new Promise((resolve) => {
        resolve(`请求url为${url}`)
      })
    }
  }
}

export namespace Request {
  export namespace Post {
    export function post(url: string, data: Record<string, unknown>): Promise<unknown> {
      return new Promise((resolve) => {
        resolve(`请求url为${url}`)
      })
    }
  }
}

类型声明

用TypeScript开发的过程中。都会新建一个types文件,里面的文件扩展名为.d.ts。如果类型声明不多的话,在顶级目录建一个types.d.ts的文件。这正是类型声明文件。类型声明的语法和常规的TS代码差不多。不过,还是有一些区别的。

  • 类型声明只含类型,不能有值。这说明,类型声明不能实现函数、类、对象或变量,参数也不能有默认值
  • 类型声明虽然不能定义值,但可以声明JS代码中定义了某个值。不过,得用declare关键字
  • 类型声明只声明使用方可见的类型。如果代码不导出,或者是函数体内的局部变量,则不为其声明类型

类型声明可以做到以下几件事:

  • 告诉TypeScript,JavaScript文件定义了某个全局变量。
  • 定义在项目中用到的类型。
  • 描述通过npm安装的第三方模块。

类型声明按照约定,如果有对应的.js文件,类型声明文件使用.d.ts扩展名;否则,使用.ts扩展名。

变量声明

外参变量声明让TypeScript知道全局变量的存在,不用显式导入即可在项目任何.ts.d.ts文件里使用。

例如,最近在用vue3和TypeScript做项目时,用到了process对象设置axiosbaseURL。编辑器提示报错process没定义,但代码在浏览器上运行也没有报错。经过百度才知道,vue3中已经将process移除了。要在vite.config.ts里面单独定义,并且要在types文件夹下添加一个process类型声明的文件。告诉TS,有一个全局对象process。编辑器的报错提示也就消失了。

declare const process: {
  env: {
    baseURL: string;
  }
}

外参变量声明不止是可以声明变量,还可以声明方法declare functiondeclare class等等。

类型声明

外参类型声明通常用于定义数据类型。例如,后端返回的数据中有哪些字段,这些字段的类型又是什么。

// request.d.ts
export interface Response {
  status: string;
  message: string;
  result: ResponseListResult | ResponseObjectResult;
}

interface ResponseListResult {
  items: [];
  pageCount: number;
  currentPage: number;
  recordCount: number;
}

interface ResponseObjectResult {
  data: Record<string, never>;
}

// index.vue
async function requestData() {
  try {
    const response: Response = await getList();
  } catch (error) {
    console.log(error);
  }
}

这样有很大的好处,在输入代码的过程中,支持TypeScript的编辑器(例如VSCode)会智能提示定义的字段。

模块声明

在使用一些第三方库时,这些库大多数都是用JS写的,并且也没有声明文件。当引入时,会提示找不到模块的声明文件。有两种解决方案,一是安装该库的声明文件(VSCode会提示你安装)。二是我们自己编写一个该库的声明文件。看下面的例子。

// index.ts
import TIM from 'tim-js-sdk'
/*
Error Could not find a declaration file for module 'tim-js-sdk'. 
'ts/node_modules/tim-js-sdk/tim-js.js' implicitly has an 'any' type.
Try `npm i --save-dev @types/tim-js-sdk` if it exists or add a new declaration (.d.ts) 
file containing `declare module 'tim-js-sdk';`
*/

引入tim-js-sdk后,提示找不到模块tim-js-sdk的声明文件。接下来,我们为它创建一个全局声明文件。

// index.ts
import TIM from 'tim-js-sdk'

// types.d.ts
declare module 'tim-js-sdk' {
  export default Object
}

这里只是为了做演示,给tim-js-sdk定义导出一个Object。此时报错就消失了。实际上这个Object类型定不定义都无所谓,直接声明一个模块也是可以的。只是说这个模块的类型是一个any。像下面这样:

// index.ts
import TIM from 'tim-js-sdk'

// types.d.ts
declare module 'tim-js-sdk'

通配符

模块声明支持通配符导入。有了通配符导入,可以给任何导入的路径声明类型。路径使用通配符*匹配即可

declare module '*.png';

declare module 'json!*' {
  let value: object;
  export default value;
}

declare module '*.css' {
  let css: CSSRuleList;
  export default css;
}

现在我们就可以引入匹配*.pngjson!**.css文件内容了。

// 加载.png文件
import logo from '../assets/logo.png';

// 加载json文件
import jsonFile from './json!File';

// 加载CSS文件
import cssFile from './index.css';
cssFile; // CSSRuleList

三斜杠指令

三斜杠指令以三条斜线///开头,三斜杠指令只能放在包含它的文件最上面。后面跟一个可用的XML标签;各XML标签有一些必须设置的属性。

types指令

types指令用于对某个包依赖。

  • 声明依赖@types/node/index.d.ts
/// <reference types="node" />

只有在你需要写一个d.ts文件时才用这个指令。

path指令

path指令和types指令类似,用于本地声明文件之间的依赖。

/// <reference path="./types/xxx.d.ts" />

最后

以上是这几个月学习TypeScript过程所做的总结。哪里有不对的地方,请各位大佬多多指点!如果文章对你有所帮助,欢迎点赞加关注哦!

参考