【夯实基础】TypeScript学习笔记

TypeScript

  • 编译环境
  • 运行环境
  • 变量声明
  • 数据类型
    • number
    • boolean
    • string
    • Array
    • Object
    • Symbol
    • null和undefined
    • any
    • unknown
    • void
    • never
    • tuple
    • 枚举类型
    • 函数类型
      • 默认参数
      • 剩余参数
      • 参数的类型注解
        • 对象类型
        • 可选类型
        • 联合类型
          • 类型缩小
            • 类型缩小方法
        • 交叉类型
        • 返回值类型
      • 匿名函数的参数
      • this的类型
        • 可推导的this类型
        • 不确定的this类型
        • 指定this的类型
      • 函数的重载
    • 字面量类型
      • 字面量推理
  • 类型断言as
  • 非空类型断言!
  • 可选链的使用
  • ??和!!的作用
    • !!操作符:
    • ??操作符
    • 类的定义
    • 类的继承
    • 类的成员修饰符
    • getters/setters
    • 静态成员
    • 抽象类abstract
    • 类的类型
  • 类型别名
  • 接口的声明
      • 索引类型
      • 函数类型
      • 接口继承
      • 接口的实现
    • interface和type区别
    • 字面量赋值
  • 泛型
    • 泛型的基本补充
    • 泛型接口
    • 泛型类
    • 泛型约束
  • 模块化开发
    • 命名空间namespace
  • 类型的查找
    • 内置类型声明
    • 外部定义类型声明和自定义声明
      • 声明变量-函数-类
      • 声明模块
      • declare文件
      • declare命名空间
  • tsconfig.json文件

TypeScript 是 Javascript的超集,它可以编译成纯 Javascript。
Typescript 可以在任何浏览器、任何计算机和任何操作系统上运行,并且是开源的

  • 优点
  1. 开发过程中,发现潜在问题,更好的错误提示
  2. 更友好的编辑器自动提示
  3. 类型的声明,可以更直观的了解语义

编译环境

  • 安装
# 安装命令
npm install typescript -g
# 查看版本
tsc --version

运行环境

  • 查看TypeScript代码的运行效果
  1. 通过tsc编译TypeScript到JavaScript代码;
  2. 在浏览器或者Node环境下运行JavaScript代码;
  • 简化步骤

通过webpack,配置本地的TypeScript编译环境和开启一个本地服务,可以直接运行在浏览器上

const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = {
  mode: 'development',
  entry: './src/main.ts',
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'bundle.js',
  },
  resolve: {
    extensions: ['.ts', '.js', '.cjs', '.json'],
  },
  devServer: {},
  module: {
    rules: [
      {
        test: /\.ts$/,
        loader: 'ts-loader',
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
    }),
  ],
};

通过ts-node库,为TypeScript的运行提供执行环境

// 安装ts-node
npm install ts-node -g
// ts-node需要依赖 tslib 和 @types/node 两个包:
npm install tslib @types/node -g
// 通过 ts-node 来运行TypeScript的代码
ts-node math.ts

变量声明

  • 声明格式

声明了类型后TypeScript就会进行类型检测,声明的类型可以称之为类型注解;
var/let/const 标识符: 数据类型 = 赋值;

let message: string = 'hello word' 
  • 注意
  1. string是TypeScript中定义的字符串类型,String是ECMAScript中定义的一个类
  2. tslint中并不推荐使用var来声明变量: var是没有块级作用域的,会引起很多的问题

数据类型

number

boolean

string

Array

const arr1: string[] = ['name']
const arr1: Array[string] = ['name']

Object

const person: object = {
	name: '张三',
	age: 18
}

Symbol

const s1: symbol = Symbol('title')
const s2: symbol = Symbol('title')

const person = {
	[s1]: 程序员,
	[s2]: 程老师
}

null和undefined

在TypeScript中,它们各自的类型也是undefined和null,也就意味着它们既是实际的值,也是自己的类型

const n: null = null
const u: undefined = undefined

any

在某些情况下,我们确实无法确定一个变量的类型,并且可能它会发生一些变化,这个时候我们可以使用any类型
any类型有点像一种讨巧的TypeScript手段
如果对于某些情况的处理过于繁琐不希望添加规定的类型注解,或者在引入一些第三方库时,缺失了类型注解,这个时候我们可以使用any

  • 特点
    1. 我们可以对any类型的变量进行任何的操作,包括获取不存在的属性、方法
    2. 我们给一个any类型的变量赋值任何的值,比如数字、字符串的值

unknown

unknown是TypeScript中比较特殊的一种类型,它用于描述类型不确定的变量

void

void通常用来指定一个函数是没有返回值的,那么它的返回值就是void类型

never

never 表示永远不会发生值的类型

function loopFn():never {
	while(true) {
		console.log('123')
	}
}

function loopError():never {
	throw new Error('123')
}

// state 0: 失败 1: 成功
function handleState(state: number) {
	switch(state) {
		case 0: return '失败';
		case 1 :return '成功';
		default: 
			return const msg:never = "不可能到这一步"
	}
}

tuple

const useInfo: [string, number, number] = ['name',  18, 1.88]
  • tuple和数组区别
  1. 首先,数组中通常建议存放相同类型的元素,不同类型的元素是不推荐放在数组中
  2. 元组中每个元素都有自己特性的类型,根据索引值获取到的值可以确定对应的类型
  • 应用场景

tuple通常可以作为返回的值,在使用的时候会非常的方便;

function useState<T>(state: T): [T, (newState: T) => void] {
	let currentState = state
	const changeState = (newState) => {
		currentState = newState
	}
	return [currentState, changeState ]
}
const [currentState, changeState] = useState(10)

枚举类型

  • 枚举其实就是将一组可能出现的值,一个个列举出来,定义在一个类型中,这个类型就是枚举类型
  • 枚举允许开发者定义一组命名常量,常量可以是数字、字符串类型
enum Direction {
  LEFT,
  RIGHT,
  TOP,
  BOTTOM
}


function turnDirection(direction: Direction) {
  switch (direction) {
    case Direction.LEFT:
      console.log("改变角色的方向向左")
      break;
    case Direction.RIGHT:
      console.log("改变角色的方向向右")
      break;
    case Direction.TOP:
      console.log("改变角色的方向向上")
      break;
    case Direction.BOTTOM:
      console.log("改变角色的方向向下")
      break;
    default:
      const foo: never = direction;
      break;
  }
}

turnDirection(Direction.LEFT)
turnDirection(Direction.RIGHT)
turnDirection(Direction.TOP)
turnDirection(Direction.BOTTOM)
  • 枚举类型默认是有值的,比如上面的枚举,默认值是这样的
  • 当然,我们也可以给枚举其他值
  • 我们也可以给他们赋值其他的类型
enum Direction {
  LEFT = "LEFT",
  RIGHT = "RIGHT",
  TOP = "TOP",
  BOTTOM = "BOTTOM"
}

let name: string = "abc"
let d: Direction = Direction.BOTTOM

function turnDirection(direction: Direction) {
  console.log(direction)
  switch (direction) {
    case Direction.LEFT:
      console.log("改变角色的方向向左")
      break;
    case Direction.RIGHT:
      console.log("改变角色的方向向右")
      break;
    case Direction.TOP:
      console.log("改变角色的方向向上")
      break;
    case Direction.BOTTOM:
      console.log("改变角色的方向向下")
      break;
    default:
      const foo: never = direction;
      break;
  }
}

turnDirection(Direction.LEFT)
turnDirection(Direction.RIGHT)
turnDirection(Direction.TOP)
turnDirection(Direction.BOTTOM)

函数类型

n 在JavaScript开发中,函数是重要的组成部分,并且函数可以作为一等公民(可以作为参数,也可以作为返回值进行传递)。
我们可以编写函数类型的表达式(Function Type Expressions),来表示函数类型

// 1.函数作为参数时, 在参数中如何编写类型
function foo() {}

type FooFnType = () => void
function bar(fn: FooFnType) {
  fn()
}

bar(foo)

// 2.定义常量时, 编写函数的类型
type AddFnType = (num1: number, num2: number) => number
const add: AddFnType = (a1: number, a2: number) => {
  return a1 + a2
}

默认参数

从ES6开始,JavaScript是支持默认参数的,TypeScript也是支持默认参数的

// 必传参数 - 有默认值的参数 - 可选参数
function foo(y: number, x: number = 20, z?: number) {
  console.log(x, y)
}

foo(30)

剩余参数

从ES6开始,JavaScript也支持剩余参数,剩余参数语法允许我们将一个不定数量的参数放到一个数组中

function sum(initalNum: number, ...nums: number[]) {
  let total = initalNum
  for (const num of nums) {
    total += num
  }
  return total
}

console.log(sum(20, 30))
console.log(sum(20, 30, 40))
console.log(sum(20, 30, 40, 50))

参数的类型注解

声明函数时,可以在每个参数后添加类型注解,以声明函数接受的参数类型:

function getUseInfo(name: string) {
	return name
}
对象类型

如果我们希望限定一个函数接受的参数是一个对象,这个时候要如何限定呢?

function printCoordinate(point: {x: number, y: number}): void {
	console.log('x坐标', point.x)
	console.log('y坐标', point.y)
}

printCoordinate({x: 10, y: 30})
  • 在这里我们使用了一个对象来作为类型:
    • 在对象我们可以添加属性,并且告知TypeScript该属性需要是什么类型
    • 属性之间可以使用 , 或者 ; 来分割,最后一个分隔符是可选的;
    • 每个属性的类型部分也是可选的,如果不指定,那么就是any类型
可选类型

对象类型也可以指定哪些属性是可选的,可以在属性的后面添加一个?
可选类型需要在必传参数的后面

function printCoordinate(point: {x: number, y: number, z?: number}): void {
	console.log('x坐标', point.x)
	console.log('y坐标', point.y)
}

printCoordinate({x: 10, y: 30})
printCoordinate({x: 10, y: 30, z: 40})
  • 补充

    可选类型可以看做是 类型 和 undefined 的联合类型

联合类型
  • 联合类型是由两个或者多个其他类型组成的类型;
  • 表示可以是这些类型中的任何一个值
  • 联合类型中的每一个类型被称之为联合成员
function printId(id: number | string) {
	console.log('你的id是:', id)
}
printId(10)
printId('string')
类型缩小
  • 定义
  • 类型缩小的英文是 Type Narrowing
  • 我们可以通过类似于 typeof padding === “number” 的判断语句,来改变TypeScript的执行路径
  • 在给定的执行路径中,我们可以缩小比声明时更小的类型,这个过程称之为 缩小
  • 而我们编写的 typeof padding === "number 可以称之为 类型保护(type guards)
类型缩小方法
  • typeof

在 TypeScript 中,检查返回的值typeof是一种类型保护:因为 TypeScript 对如何typeof操作不同的值进行编码。

type IDType = number | string
function printID(id: IDType) {
  if (typeof id === 'string') {
    console.log(id.toUpperCase())
  } else {
    console.log(id)
  }
}
  • 平等缩小

我们可以使用Switch或者相等的一些运算符来表达相等性(比如===, !==, ==, and != )

type Direction = "left" | "right" | "top" | "bottom"
function printDirection(direction: Direction) {
  // 1.if判断
  // if (direction === 'left') {
  //   console.log(direction)
  // } else if ()

  // 2.switch判断
  // switch (direction) {
  //   case 'left':
  //     console.log(direction)
  //     break;
  //   case ...
  // }
}
  • instanceof

JavaScript 有一个运算符来检查一个值是否是另一个值的“实例”

function printTime(time: string | Date) {
  if (time instanceof Date) {
    console.log(time.toUTCString())
  } else {
    console.log(time)
  }
}
  • in

Javascript 有一个运算符,用于确定对象是否具有带名称的属性:in运算符
如果指定的属性在指定的对象或其原型链中,则in 运算符返回true

class Student {
  studying() {}
}

class Teacher {
  teaching() {}
}

function work(p: Student | Teacher) {
  if (p instanceof Student) {
    p.studying()
  } else {
    p.teaching()
  }
}
交叉类型
  • 交叉类似表示需要满足多个类型的条件
  • 交叉类型使用 & 符号
interface ISwim {
  swimming: () => void
}

interface IFly {
  flying: () => void
}

type MyType1 = ISwim | IFly
type MyType2 = ISwim & IFly

const obj1: MyType1 = {
  flying() {

  }
}

const obj2: MyType2 = {
  swimming() {

  },
  flying() {
    
  }
}
返回值类型
function num (num1: number, num2: number): number {
	return number
}
  • 提示

和变量的类型注解一样,我们通常情况下不需要返回类型注解,因为TypeScript会根据 return 返回值推断函数的返回类型

匿名函数的参数

  • 匿名函数与函数声明会有一些不同:
    • 当一个函数出现在TypeScript可以确定该函数会被如何调用的地方时
    • 该函数的参数会自动指定类型
const names: string[] = ['张三', '李四', '王五']
names.forEach(item => console.log(item))
  • 我们并没有指定item的类型,但是item是一个string类型
    • 这是因为TypeScript会根据forEach函数的类型以及数组的类型推断出item的类型
    • 这个过程称之为上下文类型(contextual typing),因为函数执行的上下文可以帮助确定参数和返回值的类型

this的类型

可推导的this类型
  • TypeScript是如何处理this呢?我们先来看一个例子
// this是可以被推导出来 info对象(TypeScript推导出来)
const info = {
  name: "why",
  eating() {
    console.log(this.name + " eating")
  }
}

info.eating()

export {}
  • 上面的代码是可以正常运行的,也就是TypeScript在编译时,认为我们的this是可以正确去使用的:
    • TypeScript认为函数 sayHello 有一个对应的this的外部对象 info,所以在使用时,就会把this当做该对象
不确定的this类型
type ThisType = { name: string };

function eating(message: string) {
  console.log(this.name + " eating", message);
}

const info = {
  name: "why",
  eating: eating,
};

// 隐式绑定
info.eating("哈哈哈");
  • 这段代码运行会报错的:
    • 这里我们再次强调一下,TypeScript进行类型检测的目的是让我们的代码更加的安全
    • 所以这里对于 sayHello 的调用来说,我们虽然将其放到了info中,通过info去调用,this依然是指向info对象的
    • 但是对于TypeScript编译器来说,这个代码是非常不安全的,因为我们也有可能直接调用函数,或者通过别的对象来调用函数
指定this的类型
type ThisType = { name: string };

function eating(this: ThisType, message: string) {
  console.log(this.name + " eating", message);
}

const info = {
  name: "why",
  eating: eating,
};

// 隐式绑定
info.eating("哈哈哈");

// 显示绑定
eating.call({name: "kobe"}, "呵呵呵")
eating.apply({name: "james"}, ["嘿嘿嘿"])

函数的重载

  • 在TypeScript中,如果我们编写了一个add函数,希望可以对字符串和数字类型进行相加,应该如何编写呢
/**
 * 通过联合类型有两个缺点:
 *  1.进行很多的逻辑判断(类型缩小)
 *  2.返回值的类型依然是不能确定
 */
function add(a1: number | string, a2: number | string) {
  if (typeof a1 === "number" && typeof a2 === "number") {
    return a1 + a2
  } else if (typeof a1 === "string" && typeof a2 === "string") {
    return a1 + a2
  }
}
add(10, 20)

  • 在TypeScript中,我们可以去编写不同的重载签名(overload signatures)来表示函数可以以不同的方式进行调用;
  • 一般是编写两个或者以上的重载签名,再去编写一个通用的函数以及实现
// 函数的重载: 函数的名称相同, 但是参数不同的几个函数, 就是函数的重载
function add(num1: number, num2: number): number; // 没函数体
function add(num1: string, num2: string): string;

function add(num1: any, num2: any): any {
  if (typeof num1 === 'string' && typeof num2 === 'string') {
    return num1.length + num2.length
  }
  return num1 + num2
}

const result = add(20, 30)
const result2 = add("abc", "cba")
console.log(result)
console.log(result2)

字面量类型

// "Hello World"也是可以作为类型的, 叫做字面量类型
const message: "Hello World" = "Hello World"
// 字面量类型的意义, 就是必须结合联合类型
type Alignment = 'left' | 'right' | 'center'

let align: Alignment = 'left'
align = 'right'
align = 'center'

字面量推理

这是因为我们的对象再进行字面量推理的时候,info其实是一个 {url: string, method: string},所以我们没办法将一个 string 赋值给一个 字面量 类型。

【夯实基础】TypeScript学习笔记_第1张图片

  • 解决方法
type Method = 'GET' | 'POST'
function request(url: string, method: Method) {}

const options = {
  url: "https://www.coderwhy.org/abc",
  method: "POST"
} as const

request(options.url, options.method)

类型断言as

  • 有时候TypeScript无法获取具体的类型信息,这个我们需要使用类型断言
    • 比如我们通过 document.getElementById,TypeScript只知道该函数会返回 HTMLElement ,但并不知道它具体的类型:
const el = document.getElementById("why") as HTMLImageElement
el.src = "url地址"
  • TypeScript只允许类型断言转换为 更具体 或者 不太具体 的类型版本,此规则可防止不可能的强制转换:
const message = "Hello World"
const num: number = (message as unknown) as number

非空类型断言!

  • 确定传入的参数是有值的,这个时候我们可以使用非空类型断言:
    • 非空断言使用的是 ! ,表示可以确定某个标识符是有值的,跳过ts在编译阶段对它的检测
function printMessageLength(message?: string) {
  console.log(message!.length)
}
printMessageLength("hello world")

可选链的使用

  • 可选链事实上并不是TypeScript独有的特性,它是ES11(ES2020)中增加的特性
    • 可选链使用可选链操作符 ?.
    • 它的作用是当对象的属性不存在时,会短路,直接返回undefined,如果存在,那么才会继续执行
    • 虽然可选链操作是ECMAScript提出的特性,但是和TypeScript一起使用更版本
type Person = {
  name: string
  friend?: {
    name: string
    age?: number,
    girlFriend?: {
      name: string
    }
  }
}

const info: Person = {
  name: "why",
  friend: {
    name: "kobe",
    girlFriend: {
      name: "lily"
    }
  }
}

console.log(info.name)
console.log(info.friend?.name)
console.log(info.friend?.age)
console.log(info.friend?.girlFriend?.name)

??和!!的作用

!!操作符:

  • 将一个其他类型转换成boolean类型
  • 类似于Boolean(变量)的方式
const message = "Hello World"

const flag = !!message
console.log(flag) // true

??操作符

  • 它是ES11增加的新特性
  • 空值合并操作符(??)是一个逻辑操作符,当操作符的左侧是 null 或者 undefined 时,返回其右侧操作数,否则返回左侧操作数;
let message: string|null = "Hello World"

const content = message ?? "你好啊"
// const content = message ? message: "你好啊"
console.log(content)

在早期的JavaScript开发中(ES5)我们需要通过函数和原型链来实现类和继承,从ES6开始,引入了class关键字,可以 更加方便的定义和使用类
TypeScript作为JavaScript的超集,也是支持使用class关键字的,并且还可以对类的属性和方法等进行静态类型检测。

类的定义

  • 用class关键字来定义一个类
  • 我们可以声明一些类的属性:在类的内部声明类的属性以及对应的类型
    • 如果类型没有声明,那么它们默认是any的
    • 我们也可以给属性设置初始化值
    • 在默认的strictPropertyInitialization模式下面我们的属性是必须初始化的,如果没有初始化,那么编译时就会报错
    • 如果我们在strictPropertyInitialization模式下确实不希望给属性初 始化,可以使用 name!: string语法
  • 类可以有自己的构造函数constructor,当我们通过new关键字创建一个 实例时,构造函数会被调用
    • 构造函数不需要返回任何值,默认返回当前创建出来的实例
  • 类中可以有自己的函数,定义的函数称之为方法
class Person {
  name: string
  age: number

  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }

  eating() {
    console.log(this.name + " eating")
  }
}

const p = new Person("why", 18)
console.log(p.name)
console.log(p.age)
p.eating()

类的继承

  • 我们使用extends关键字来实现继承,子类中使用super来访问父类
  • 我们来看一下Student类继承自Person
    • Student类可以有自己的属性和方法,并且会继承Person的属性和方法
    • 在构造函数中,我们可以通过super来调用父类的构造方法,对父类中的属性进行初始化
class Person {
  name: string
  age: number

  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }

  eating() {
    console.log("eating 100行")
  }
}

class Student extends Person {
  sno: number

  constructor(name: string, age: number, sno: number) {
    // super调用父类的构造器
    super(name, age)
    this.sno = sno
  }

  eating() {
    console.log("student eating")
    super.eating()
  }

  studying() {
    console.log("studying")
  }
}

const stu = new Student("why", 18, 111)
console.log(stu.name)
console.log(stu.age)
console.log(stu.sno)

stu.eating()

类的成员修饰符

  • 在TypeScript中,类的属性和方法支持三种修饰符: public、private、protected
    • public 修饰的是在任何地方可见、公有的属性或方法,默认编写的属性就是public的
    • private 修饰的是仅在同一类中可见、私有的属性或方法
    • protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法
  • public是默认的修饰符,也是可以直接访问的
  • 只读属性readonly
    • 如果有一个属性我们不希望外界可以任意的修改,只希望确定值后直接使用,那么可以使用readonly
class Person {
  // 1.只读属性是可以在构造器中赋值, 赋值之后就不可以修改
  // 2.属性本身不能进行修改, 但是如果它是对象类型, 对象中的属性是可以修改
  readonly name: string
  age?: number
  readonly friend?: Person
  constructor(name: string, friend?: Person) {
    this.name = name
    this.friend = friend
  }
}

const p = new Person("why", new Person("kobe"))
console.log(p.name)
console.log(p.friend)

// 不可以直接修改friend
// p.friend = new Person("james")
if (p.friend) {
  p.friend.age = 30
} 

// p.name = "123"

getters/setters

在前面一些私有属性我们是不能直接访问的,或者某些属性我们想要监听它的获取(getter)和设置(setter)的过程, 这个时候我们可以使用存取器。

class Person {
  private _name: string
  constructor(name: string) {
    this._name = name
  }

  // 访问器setter/getter
  // setter
  set name(newName) {
    this._name = newName
  }
  // getter
  get name() {
    return this._name
  }
}

const p = new Person("why")
p.name = "coderwhy"
console.log(p.name)

静态成员

  • 前面我们在类中定义的成员和方法都属于对象级别的, 在开发中, 我们有时候也需要定义类级别的成员和方法
  • 在TypeScript中通过关键字static来定义
class Student {
  static time: string = "20:00"

  static attendClass() {
    console.log("去学习~")
  }
}

console.log(Student.time)
Student.attendClass()

抽象类abstract

  • 我们知道,继承是多态使用的前提
    • 所以在定义很多通用的调用接口时, 我们通常会让调用者传入父类,通过多态来实现更加灵活的调用方式
    • 但是,父类本身可能并不需要对某些方法进行具体的实现,所以父类中定义的方法,,我们可以定义为抽象方法
  • 什么是 抽象方法? 在TypeScript中没有具体实现的方法(没有方法体),就是抽象方法
    • 抽象方法,必须存在于抽象类中
    • 抽象类是使用abstract声明的类
  • 抽象类有如下的特点
    • 抽象类是不能被实例的话(也就是不能通过new创建)
    • 抽象方法必须被子类实现
function makeArea(shape: Shape) {
  return shape.getArea()
}

abstract class Shape {
  abstract getArea(): number
}


class Rectangle extends Shape {
  private width: number
  private height: number

  constructor(width: number, height: number) {
    super()
    this.width = width
    this.height = height
  }

  getArea() {
    return this.width * this.height
  }
}

class Circle extends Shape {
  private r: number

  constructor(r: number) {
    super()
    this.r = r
  }

  getArea() {
    return this.r * this.r * 3.14
  }
}

const rectangle = new Rectangle(20, 30)
const circle = new Circle(10)

console.log(makeArea(rectangle))
console.log(makeArea(circle))

类的类型

class Person {
  name: string = "123"
  eating() {

  }
}

const p = new Person()

const p1: Person = {
  name: "why",
  eating() {

  }
}

function printPerson(p: Person) {
  console.log(p.name)
}

printPerson(new Person())
printPerson({name: "kobe", eating: function() {}})

类型别名

type ID = number | string
function printId(id: ID) {
	console.log('你的id是:', id)
}
printId(10)

接口的声明

// 通过类型(type)别名来声明对象类型
// type InfoType = {name: string, age: number}

// 另外一种方式声明对象类型: 接口interface
// 在其中可以定义可选类型
// 也可以定义只读属性
interface IInfoType {
  readonly name: string
  age: number
  friend?: {
    name: string
  }
}

const info: IInfoType = {
  name: "why",
  age: 18,
  friend: {
    name: "kobe"
  }
}

console.log(info.friend?.name)
console.log(info.name)
// info.name = "123"
info.age = 20

索引类型

// 通过interface来定义索引类型
interface IndexLanguage {
  [index: number]: string
}

const frontLanguage: IndexLanguage = {
  0: "HTML",
  1: "CSS",
  2: "JavaScript",
  3: "Vue"
}


interface ILanguageYear {
  [name: string]: number
}

const languageYear: ILanguageYear = {
  "C": 1972,
  "Java": 1995,
  "JavaScript": 1996,
  "TypeScript": 2014
}

函数类型

// type CalcFn = (n1: number, n2: number) => number
// 可调用的接口
interface CalcFn {
  (n1: number, n2: number): number
}

function calc(num1: number, num2: number, calcFn: CalcFn) {
  return calcFn(num1, num2)
}

const add: CalcFn = (num1, num2) => {
  return num1 + num2
}

calc(20, 30, add)
  • 除非特别的情况,还是推荐大家使用类型别名来定义函数

接口继承

  • 接口和类一样是可以进行继承的,也是使用extends关键字
    • 并且我们会发现,接口是支持多继承的(类不支持多继承)
interface ISwim {
  swimming: () => void
}

interface IFly {
  flying: () => void
}


interface IAction extends ISwim, IFly {

}

const action: IAction = {
  swimming() {

  },
  flying() {
    
  }
}

接口的实现

  • 接口定义后,也是可以被类实现的
    • 如果被一个类实现,那么在之后需要传入接口的地方,都可以将这个类传入
    • 这就是面向接口开发
interface ISwim {
	swimming: () => void
}

interface IRun {
	running: () => void
}

class Person implements ISwim,IRun {
	swimming() {
		console.log('swimming')
	},
	running() {
		console.log('running')
	}
}

function swim(swimmer: ISwim) {
	swimmer.swimming()
}

const p = new Person()

swim(p)

interface和type区别

  • 如果是定义对象类型,那么他们是有区别的
    • interface 可以重复的对某个接口来定义属性和方法
    • 而type定义的是别名,别名是不能重复的
interface IFoo {
  name: string
}

interface IFoo {
  age: number
}

const foo: IFoo = {
  name: "why",
  age: 18
}


type IBar = {
  name: string
  age: number
}

// type IBar = {
// }

字面量赋值

interface IPerson {
  name: string
  age: number
  height: number
}
function printInfo(person: IPerson) {
  console.log(person)
}
const info = {
  name: "why",
  age: 18,
  height: 1.88,
  address: "广州市"
}

printInfo(info)
  • 这是因为TypeScript在字面量直接赋值的过程中,为了进行类型推导会进行严格的类型限制
    • 但是之后如果我们是将一个 变量标识符 赋值给其他的变量时,会进行freshness擦除操作

泛型

  • 提一个需求:封装一个函数,传入一个参数,并且返回这个参数
  • 要求使用一种特性的变量 - 类型变量(type variable),它作用于类型,而不是值
function sum<Type>(num: Type): Type {
  return num
}
  • 调用方式一: 明确的传入类型
sum<number>(20)
sum<{name: string}>({name: "why"})
sum<any[]>(["abc"])
  • 调用方式二: 类型推到
sum(50)
sum("abc")

泛型的基本补充

  • 传入多个类型
function foo<T, E, O>(arg1: T, arg2: E, arg3?: O, ...args: T[]) {

}

foo<number, string, boolean>(10, "abc", true)
  • 开发中我们可能会看到一些常用的名称
    • T:Type的缩写,类型
    • K、V:key和value的缩写,键值对
    • E:Element的缩写,元素
    • O:Object的缩写,对象

泛型接口

interface IPerson<T1 = string, T2 = number> {
  name: T1
  age: T2
}

const p: IPerson = {
  name: "why",
  age: 18
}

泛型类

class Point<T> {
  x: T
  y: T
  z: T

  constructor(x: T, y: T, z: T) {
    this.x = x
    this.y = y
    this.z = y
  }
}

const p1 = new Point("1.33.2", "2.22.3", "4.22.1")
const p2 = new Point<string>("1.33.2", "2.22.3", "4.22.1")
const p3: Point<string> = new Point("1.33.2", "2.22.3", "4.22.1")

泛型约束

  • 有时候我们希望传入的类型有某些共性,但是这些共性可能不是在同一种类型中
    • 比如string和array都是有length的,或者某些对象也是会有length属性的
    • 那么只要是拥有length的属性都可以作为我们的参数类型,那么应该如何操作呢
interface ILength {
  length: number
}

function getLength<T extends ILength>(arg: T) {
  return arg.length
}

getLength("abc")
getLength(["abc", "cba"])
getLength({length: 100})

模块化开发

  • TypeScript支持两种方式来控制我们的作用域
    • 模块化:每个文件可以是一个独立的模块,支持ES Module,也支持CommonJS
    • 命名空间:通过namespace来声明一个命名空间

命名空间namespace

命名空间在TypeScript早期时,称之为内部模块,主要目的是将一个模块内部再进行作用域的划分,防止一些命名 冲突的问题。

export namespace time {
  export function format(time: string) {
    return "2222-02-22"
  }

  export function foo() {

  }

  export let name: string = "abc"
}

类型的查找

  • 之前我们所有的typescript中的类型,几乎都是我们自己编写的,但是我们也有用到一些其他的类型,
const imageEl = document.getElementById('images') as HTMLImageElement
  • 大家是否会奇怪,我们的HTMLImageElement类型来自哪里呢?甚至是document为什么可以有getElementById的方法呢?
    • 其实这里就涉及到typescript对类型的管理和查找规则了
  • 我们这里先给大家介绍另外的一种typescript文件:.d.ts文件
    • 我们之前编写的typescript文件都是 .ts 文件,这些文件最终会输出 .js 文件,也是我们通常编写代码的地方
    • 还有另外一种文件 .d.ts 文件,它是用来做类型的声明(declare)。 它仅仅用来做类型检测,告知typescript我们有哪 些类型
  • 那么typescript会在哪里查找我们的类型声明呢
    • 内置类型声明
    • 外部定义类型声明
    • 自己定义类型声明

内置类型声明

  • 内置类型声明是typescript自带的、帮助我们内置了JavaScript运行时的一些标准化API的声明文件
    • 包括比如Math、Date等内置类型,也包括DOM API,比如Window、Document等
  • 内置类型声明通常在我们安装typescript的环境中会带有的
    • https://github.com/microsoft/TypeScript/tree/main/lib

外部定义类型声明和自定义声明

  • 外部类型声明通常是我们使用一些库(比如第三方库)时,需要的一些类型声明
  • 这些库通常有两种类型声明方式
    • 方式一:在自己库中进行类型声明(编写.d.ts文件),比如axios
    • 方式二:通过社区的一个公有库DefinitelyTyped存放类型声明文件
      1. GitHub地址
      2. 查找声明安装方式的地址
      3. 比如我们安装react的类型声明: npm i @types/react --save-dev
  • 什么情况下需要自己来定义声明文件呢
    • 情况一:我们使用的第三方库是一个纯的JavaScript库,没有对应的声明文件;比如lodash
    • 情况二:我们给自己的代码中声明一些类型,方便在其他地方直接进行使用

声明变量-函数-类

// 声明变量/函数/类
declare let whyName: string
declare let whyAge: number
declare let whyHeight: number

declare function whyFoo(): void

declare class Person {
  name: string
  age: number
  constructor(name: string, age: number)
}

// 声明命名空间
declare namespace $ {
  export function ajax(settings: any): any
}

声明模块

  • 我们也可以声明模块,比如lodash模块默认不能使用的情况,可以自己来声明这个模块
// 声明模块
declare module 'lodash' {
  export function join(arr: any[]): void
}
  • 声明模块的语法: declare module ‘模块名’ {}。
    • 在声明模块的内部,我们可以通过 export 导出对应库的类、函数等;

declare文件

  • 在某些情况下,我们也可以声明文件
    • 比如在开发vue的过程中,默认是不识别我们的.vue文件的,那么我们就需要对其进行文件的声明
    • 比如在开发中我们使用了 jpg 这类图片文件,默认typescript也是不支持的,也需要对其进行声明
declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}
// 声明文件
declare module '*.jpg'
declare module '*.jpeg'
declare module '*.png'
declare module '*.svg'
declare module '*.gif'

declare命名空间

  • 比如我们在index.html中直接引入了jQuery
    • CDN地址: https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js
  • 我们可以进行命名空间的声明
// 声明命名空间
declare namespace $ {
  export function ajax(settings: any): any
}
  • 在main.ts中就可以使用了
$.ajax({})

tsconfig.json文件

  • tsconfig.json是用于配置TypeScript编译时的配置选项
  • https://www.typescriptlang.org/tsconfig

你可能感兴趣的:(typeScript,typescript,前端)