Skip to content

什么是 JavaScript?一个简短的历史

再具体的展开讲解typescript之前,我们要来介绍下跟它

about typescript

typescript英文官方网站:https://www.typescriptlang.org/

typescritp中文网:https://ts.nodejs.cn/

typescript

  • 拼写错误
  • 函数未执行
  • 基本的逻辑错误等等。

typescript环境搭建

bash
# 全局安装
npm i typescript -g

# 返回版本表示安装成功
D:\mydata\myclass\web\typescript>tsc -v
Version 5.0.4

tsconfig.json

tsc --init生成配置文件

降级编译

tsconfig.json文件中,有个target参数,它的值默认是es2016,当将ts文件编译成js文件的时候,它的编译规则依据就是target的值来的。

当考虑浏览器兼容性的时候,比如某些浏览器不支持es6语法,那么我们就可以修改target值来进行降级编译,例如修改为es3

严格模式

json
{
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true
}

noImplicitAny

js中默认函数的参数可以是任意类型,但ts中的严格模式下,你就就要为参数指定其类型,否则就会抛出implicitly的错误。

这个错误我们也可以在ts配置文件中进行配置,如果你声明"noImplicitAny": false,那么ts就不在提示这个错误了。

tsx
// Parameter 'person' implicitly has an 'any' type.
// Parameter 'date' implicitly has an 'any' type.
function foo(person, date) {  
    console.log(`你好: ${person}, 今天是: ${date}`)
}

strictNullChecks

默认的undefined和null可以赋值给任意类型,如下示例,但这在逻辑上是有问题,所以在ts的配置文件中可以设置"strictNullChecks": true来进行规范这种问题。

tsx
let user: string = undefined  // Type 'undefined' is not assignable to type 'string'.
let age: number = null        // Type 'null' is not assignable to type 'number'.

strict

ts的配置文件中默认是"strict": true的,也就是默认就是严格模式,我们可以通过设置为false的形式取消严格模式,但这样的话,ts的作用也就没啥意义了,所以,这个仅做了解。

上面两个noImplicitAnystrictNullChecks这两个参数完全可以不指定,只要设置"strict": true就默认有了上面两个参数的效果了。

数据类型相关

基础数据类型

ts跟js一样,支持三种基础的数据类型,也叫做原始的数据类型,可以单独使用,也可以和其他数据类型结合组成新的复杂的类型。

tsx
string
	就是字符串了
number
	包含正负值、0、小数这些都是number类型
boolean
	true
	false
	
let str: string = 'hello'
let num: number = 123
let bool: boolean = true

关于number,除了表示常用的十进制,还支持:

tsx
let a1: number = 10      // 十进制表示10
let a2: number = 0o12    // 八进制表示10
let a3: number = 0xa     // 十六进制表示10
let a4: number = 0b1010  // 二进制表示10

数组

数组定义的两种方法:

typescript
// 定义空数组
let arr1: [] = [];

// 定义数组,且数组内部的数据必须是数字类型
// 但没有初始值
let arr2: number[] = [];

// 定义数组,且数组内部的数据必须是数字类型,有初始值
let arr3: number[] = [1, 2, 3];

// 泛型的形式定义数据
let arr4: Array<number> = [];
let arr5: Array<number> = [1, 2, 3];

// 我们可以将数组置为空,但不能包含其它的类型的元素
// arr1.push('a')  // Argument of type 'string' is not assignable to parameter of type 'number'.
arr1 = []  // 置为空是可以的

对象

typescript
// 该函数接收一个对象类型的参数,且对象中的两个值类型必须是number类型,不能少传,也不能多传
// 对象中的键值对分割可以是逗号,也可以是分号
//      obj: {x: number, y: number}
//      obj: {x: number; y: number}
//      末尾的分号或者逗号可以省略不写
//          obj: {x: number; y: number;}
//          obj: {x: number; y: number,}

function foo1(obj: {x: number, y: number}) {
    console.log(obj.x, obj.y)
}
foo1({x: 1, y:2}) // 一一对应且类型相同
// foo1({x: 1})  // 不能少传
// foo1({x: 1, y: 2, z: 3})  // 不能多传


// 问号语法表示y这个参数可传就必须传number类型,可不传,不传的话,ts就会推断为undefined类型
function foo2(obj: {x: number, y?: number}) {
    // 可传可不传的,在内部操作起来要注意,不能一视同仁
    console.log(obj.x, obj.y)
}
foo2({x: 1})  // y可不传
foo2({x: 1, y: 2})  // 传就必须传number类型

any

ts的严格模式会进行错误类型检查,通不过会报错。

当你不需要对某个值进行特定类型检查时,也就是不希望报错时,可以指定为any类型,比如:

typescript
let obj: any = {
    age: 18
}
// 很显然obj对象中没有func这个函数,但是因为定义obj时指定了any类型,那么ts类型检查就不报错
// 可以正常编译为js文件,但注意,编译后的js文件执行时会报错,因为obj中真没有func
// 所以any类型使用时要慎重
obj.func()

类型推断

typescript
// 定义变量时,可以通过显式的声明该变量的类型
let s1: string = "hello"

// 如果不声明类型,那么ts会自动的根据值的类型来推断这个变量的类型
let s2 = "hello"

// 注意,如果是函数的参数,如果不显示的声明参数的类型,那么ts会推断为any类型,这点要注意
// Parameter 'name' implicitly has an 'any' type.
function foo(name) {
    console.log(name)
}

foo('zhangkai')

类型声明

类型声明,也叫做类型注释,就是声明的变量后面跟的: string这部分。

typescript
let s1: string = "hello"

类型声明有参数类型声明和返回值类型声明两种:

typescript
// name为参数类型声明
function foo1(name: string) {
    console.log(name)
}

// 括号后面的: number 叫做返回值类型的声明,表示该函数必须有返回值,且返回值的类型必须是number
function foo2(name: string): number {
    console.log(name)
    return 1
    // 返回别的类型报错,不写返回值也报错
    // return 'a'  // Type 'string' is not assignable to type 'number'.
}

// 括号后面的: void  表示该函数没有返回值
function foo3(name: string): void {
    console.log(name)
    // void表示该函数没有返回值,如果你写return的话,就报错
    // return 1  // Type 'number' is not assignable to type 'void'.
}

// 括号后面的不跟冒号和类型,那么ts在编译时会根据return的值的类型进行推断
function foo4(name: string) {
    console.log(name)
    return name
}

联合(unique)类型

typescript
// 结合管道符,实现联合类型,即x的值类型可以是string或者是number
// 当然,也可以联合多个类型 x: string | number | number[] .....
function foo1(x: string | number) {
    // 不同的类型在内部操作起来要注意,不能一视同仁
    console.log(x)
}
foo1('a')
foo1(1)

类型别名和接口

类型别名

类型别名可以解决重复类型重复声明的问题:

typescript
// 通过type来定义类型别名,通常类型别名是首字母大写形式,一经定义,后续就可以重复使用这个类型别名了
type Point = {
    x: number
    y: number
}
// 可以定义string、number、array甚至是联合类型
type Str1 = string

// 注意type声明的是类型别名的名字,而不是在定义变量,等号后面是具体的类型,千万不要认为是在定义一个变量
type Num1 = number
type Arr1 = number[]
type Unique = number | string

// 用法如下
const obj: Point = {x:10, y:100}  // 定义变量
// 定义形参
function foo1(obj: Point) {
    console.log(obj.x, obj.y)
}

function foo2(s1: Str1) {
    console.log(s1)
}

// 也可以用于返回值类型中
function foo3(s1: Str1): Num1 {
    console.log(s1)
    return 1
}

扩展类型别名

typescript
// 定义类型别名,相当于Python定义父类,我们可以将公共的部分写入到父类Animal中
type Animal = {
    name: string
}
const dog1: Animal = {name: '小黑'}

// 在Animal类型别名的基础上进行扩展一个新的类型别名,类似于定义一个子类,用于定义独有的功能,然后继承父类
// 其语法如下,通过"父类" 类型别名加 交叉点&符号 来指定在哪个 "父类" 的基础上进行扩展
type Dog = Animal & {
    age: number
}
const dog2: Dog = {name: '小黑', age: 18}
console.log(dog2.name, dog2.age)

接口

接口是ts中定义类型的一种,通过关键字interface来定义。

定义接口

接口的定义和使用跟类型别名差不多,使用上也基本类似。

typescript
// 通过interface关键字声明一个接口,关键字后面跟接口名称,然后不需要加等于号直接花括号
interface Point {
    x: number
    y: number
}

// 用法如下
const obj: Point = {x:10, y:100}
// 定义形参
function foo(obj: Point) {
    console.log(obj.x, obj.y)
}

扩展接口

typescript
// 接口有点类似于Python定义父类,我们可以将公共的部分写入到父类Animal中
interface Animal {
    name: string
}
const dog1: Animal = {name: '小黑1'}

// 扩展接口,相当于定义一个子类,用于定义独有的功能,然后继承父类
// 只不过ts中这类套路称之为接口
// 通过interface声明子类,然后通过extents后跟你要扩展的接口名称
// 扩展后得到新的接口
interface Dog extends Animal {
    age: number
}
const dog2: Dog = {name: '小黑2', age: 18}

类型别名和接口的区别

通过上面几个示例,我们可以发现,类型别名和接口非常相似,我们做个小结:

  • 几乎所有的能通过interface来定义的类型,都可以通过type类型别名来实现。
  • 接口可以通过extents的方式进行扩展出一个新的接口;而类型则可以通过交叉点&的方式来实现扩展出来一个新的类型别名。

区别也是有的,那就是类型别名一经定义,无法改变,也就是无法在原有的类型别名上进行扩展其本身,但接口可以:

typescript
// 定义一个类型别名
type Point = { x: number, y: number }
// 我们想为Point类型别名进行扩展,但这种做法是错误,标识符 'Point' 是重复的  Duplicate identifier 'Point'.
// type Point = { z: number }


// 但接口可以,首先我们定一个一个接口Animal
interface Animal {name: string}
// 然后可以接续通过interface的方式扩展Animal接口
interface Animal {age: number}

const dog: Animal = {name: '小黑', age: 18}
console.log(dog.name, dog.age)

相对来说,接口更灵活一些。

类型断言

有的时候我们不能确定一个对象的类型,比如我们就能十成十的说我们拿到的标签就是div么?不能,所以类型断言就是将一个对象断言为一个"差不多"的类型。

typescript
// 下面两个语句是等价
// 通过 as 断言获取的标签是div element
const myMain1 = document.getElementById('main') as HTMLDivElement

// 通过 尖括号 断言获取的标签是div element
const myMain2 = <HTMLDivElement>document.getElementById('main')

文字类型

不好整理,先跳过:https://www.bilibili.com/video/BV1H44y157gq?p=23&vd_source=f56f96c7f7894594fdc04129b7d97ff6

null和undefined

null表示不存在

undefined表示未初始化的值。

ts
let a1: undefined = undefined 
let a2: null = null

枚举类型

不太常用的原语bigint和symbol

类型缩小

类型缩小就是一个不断判断一个对象的具体类型的过程,

特殊的检查称之为类型防护,将类型细化为比声明时更具体的类型的过程,称之为类型缩小。

类型守卫type of

真值缩小

在js中,可以通过如下几种情况组成的表达式来判断一个值的类型,从而达到类型缩小的的目的:

条件   &&   ||  if语句   布尔否定(!)
typescript
// 在ts中,如下几个对象的布尔值是false
// 0  NaN  ""(空字符串)   0n(bigint中的0)  null   undefined  
// 注意只有ES2020才有的bigint



// 我们可以通过两种方式来返回一个对象的布尔值
console.log(Boolean(0))
console.log(Boolean(NaN))
console.log(Boolean(""))
console.log(Boolean(null))
console.log(Boolean(undefined))

// 也可以通过两个感叹号来实现,这两个感叹号,右边的表示将空字符串转换为文字布尔值,左边的感叹号再将文件布尔值转换为布尔值类型
console.log(!!'')

等值缩小

ts中 可以使用一下几个运算符来实现全等、全不等、等于、不等于来做等值检查,实现类型缩小:

typescript
===   !==   ==   !=

in操作符缩小

instanceof操作符缩小

typescript
function foo(x: Date | string) {
    if (x instanceof Date){
        console.log(x.toLocaleDateString())
    }else {
        console.log(x.toUpperCase())
    }
}
foo(new Date())  // 2023/5/21
foo('hello')  // HELLO

分配缩小

typescript
// 如下表达式的返回值可能是number也可能是string
let x = Math.random() < 0.5 ? 10: 'hello'
// 所以在ts内部,会进行分配缩小定义x的值类型为联合类型
// let x = number | string

// 所以,下面的情况都是允许的,即为x赋值为number类型的值或者为string类型的值
x = 10
x = 'abc'
// x = true   // 这个就不行

控制流分析

如下示例,基于代码的可达性分析称之为控制流分析,通过控制流分析也能达到类型缩小的目的。

typescript
function foo(x: number | string) {
    // 当x的类型是number时,就会走if分支,那么可以断定这个函数的返回值就是number类型
    // 那就意味着 return x + x 这段代码永远不会执行,也就是不可达的
    if (typeof x === 'number') {
        return x
    }
    return x + x
}
foo(2)

当一个变量被分析时,控制流可以一次又一次的分裂和重新合并,该变量在可以被观察的每一个点上,具备不同的类型。这句话有点拗口,来看例子:

typescript
function foo() {
    let x: string | number | boolean

    // 当代码执行到下面这句时,x的类型为布尔类型 x: boolean
    x = Math.random() < 0.5

    if (Math.random() < 0.5){
        // 如果if条件成立的话,x的类型将是string类型
        x = 'hello'
    }else{
        // 如果走这里,那么x的类型将是number类型
        x = 100
    }
    // 问题来了,如果代码走到这里,return x的值的类型是什么呢?
    // 是 number?  string?  boolean?
    // 还是联合类型 string | number | boolean
    return x
}
// 我们来做个实验,首先拿到函数的返回值x
let x = foo()
// 将x分别赋值为string和number都不报错
x = 'hello'
x = 100

// 但将其赋值为boolean类型就报错了,根据报错内容来看,说布尔类型不能赋值给'string | number'这个联合类型
// 此时的x的类型为联合类型'string | number',为什么会是这样呢?
// 答案在函数体内,当程序执行完函数内部的if代码块后,x的类型要么是number要么是string,也就是被推断为'string | number'这个联合类型
// 无论如何它再也不可能是布尔类型了
// 所以,下面这个x = true的报错也就可以理解了
// x = true  // Type 'boolean' is not assignable to type 'string | number'.

类型谓词

受歧视的unique

never类型与穷尽性检查

never类型表示不存在的状态。

never类型可以分配给其他任何类型,但没有任何类型可以分配给never和never本身。

函数

签名

构造签名