【青训营】- TypeScript

427 阅读9分钟

TypeScript

初始化第一个TypeScript项目

首先创建文件夹TS,在vscode中打开该文件夹,我习惯使用vs里面集成的终端,所以下一步是在vs中打开终端,初始化项目生成package.json文件

npm init -y

第二步安装ts依赖

npm i typescript -S   #最新稳定版

上面是安装最新的稳定版,如果想尝试最新开发版,可以使用下面代码指令安装

npm i typescript@next -S   #nightly build

image-20210901154842117.png

接下来我们要创建tsconfig.json文件,是ts的配置文件

node_modules/.bin/tsc --init --locale zh-CN

在我们刚才安装的依赖包里面bin目录下有一个tsc文件,可以使用上面的命令帮助我们创建tsconfig.json文件

image-20210901155817198.png

这个config文件包含所有ts可配置的选项,后面也有相应的注释,帮助我们更好的配置ts

image-20210901155950528.png

把鼠标悬停在属性上面,vs会给我们展示该属性的解释和提示以及官方文档的链接,点击查看更详细的解释

如何选择ts版本

image-20210901160702516.png 点击注脚的ts版本可以对ts版本进行设置

image-20210901160742616.png

右边三个分别是使用vs自带的ts版本,使用工作区的版本,使用刚才安装的nodemodules里面的版本

当我们选择nodemodules中的版本时,在工作区会生成一个vs的settings.json文件,里面指定了当前ts的tsdk的版本来源,指向了我们安装的ts包中的lib文件夹内

image-20210901161044551.png

TS与JS的关系

typescript是JavaScript的超集

typescript提供所有JavaScript的特性,并在其上层增加了typescript类型系统

这个类型系统被设计为 "可选的" .这就意味着:所有的合法JavaScript代码都是合法的typescript代码

image-20210901151737107.png

TS的编译过程

image-20210901152131816.png

TS的类型检查与生成JS是两个独立的过程,类型检查出错不影响生成的JavaScript代码

我们知道,C++这种编译型语言,当我们在编辑器或者IDE中编辑好代码,点击build或者构建的时候,我们的代码会送给g++,g++进行读取相应的头文件,编译失败会报错,编译成功会生成对应的可执行性文件给操作系统去执行.

那我们的TS文件在编译的时候,首先在编辑器中编辑,如果出现错误,会由TSServer给我们提示,点击编译会发给tsc,tsc会在相关的模块中查找使用到的模块进行输出.这里跟c++这类语言不同的是,ts在编译过程中如果出现报错,也会生成js文件.

类型系统

强类型的编程语言中都会有类型系统,大致分为一下两类

结构类型系统(Structural Type System)

通过类型的实际结构确定两个类型是否相等或兼容

(TypeScript,Haskell,OCaml,Go,...)

看一下示例代码

class Foo {
    public hi(){
        console.log("hi")
    }
}
​
class Bar {
    public hi(){
        console.log("hello")
    }
}
​
const a : Foo = new Foo()
const b : Bar = a
b.hi()   //works!

我们看到,在ts的代码中,两个不同的类Foo和Bar,虽然类名不一样,但是整体结构一样,都有一个名为"hi"的方法,所以两个类可以互相赋值

名义类型系统(Nominal Type System)

通过类型的名称确定两个类型是否相等

(C,C++,Java,C#,...)

#include <iostream>
class Foo {
    public:
        void hi() { std :: cout << "Hi" << std :: endl;}
};
class Bar {
    public:
        void hi() { std :: << "Hello" << std::endl;}
}
int main (){
    Foo foo;
    Foo & fooRef = foo; //works!
    //Error:type 'Bar' cannot
    //bind to a value of unrelated type 'Foo'
    Bar & bar = foo;
    return 0;
}

但是在C++这种名义类型的语言中,虽然Foo和Bar结构相同,但是类名不同,不能完成赋值

类型注解

不同风语言的类型注解语法

// C++
int printf (const char*, ...)
​
# Objective-c
- (id)initWinthInt:(int)value;
​
//Julia
commission(sale::Int , rate::Float64)::Float64
​
#TypeScript
function log(message: string):void

前面两种语言的类型注解是放在类型的前面

而Juila的注解是放在后面,用两个冒号链接

TS的也是在后面,但是是用一个冒号链接

类型与集合的关系

空集(Empty Type)

never = {}

单元素集合(Unit Type)

Null = {null}
Undefined = {undefined}
Literal Type(字面量类型,'a',1,true)

有限集合

Boolean = {true,false}

无限集合(近似)

String = {'a','b', ... 'hello', ...}
Symbol = {Symbol(...), ...}
BigInt = {..., -1n,0n,1n,...}
Number = {-Infinity,-(2^53 - 1),...0,...+(2^53 - 1),Infinity} 和 NaN

全集(Universal Set)

image-20210901163457232.png

类型联合与交叉

名字联合类型(Union Types) 集合并集(Unio)交叉类型(Intersection Types) 集合交集(Intersection)
图示image-20210901163737100.pngimage-20210901163747091.png
TS写法 数学写法AB (A或B) A U B (A并B)A & B (A与B) A ^ B (A交B)

类型别名

在JS中我们可以使用let, const, var声明变量或常量

在TS中我们可以使用type为类型声明别名

// Value Space
let ID = 9527
const PI = 3.14
var myPI = PI// Type Space
type ID = string
type User = {
    id:ID;
    name:string
}
​
//Error: Duplicate identifier 'User'
type User = {}
​
//块级作用域
{
    //OK
    type User = {
        id:number;
        name:string
    }
}
​

上面我们可以看到TS的声明会用type,类型别名和let变量类似,也采用块级作用域.所以,同一个作用域内不能重名

当我们用{}括起来的块级作用域内就可以使用与外接想要同的别名,与let作用域相同

类型的拓宽与收窄

类型拓宽(Type Widening)

当你用字面量赋值给let,var变量时,TS不用字面量类型作为该变量的类型,而是从字面量类型拓宽到响应的更宽泛的类型,这个过程叫做类型拓宽

例如我们使用type定义给let或var变量时,不会仅限于字面量类型,number或者string类型会进行更宽泛的类型

类型收窄(Type Narrowing)

在某些情况下,TS可以更加确定变量的类型,此时它会将变量类型收窄

TS试图在类型确定性与灵活性之前取得平衡

TS提供一系列方法来帮助收窄类型,以提高类型的确定性:

null check,as const,instanceof,typeof

属性检查,Tagged Union ,用户类型守卫(User-defined Type Guard),代码流分析

用let和var声明变量时,TS认为变量未来会发生改变,所以将类型推断为相对宽泛的类型

用const声明变量时,TS知道常量不会改变,会将类型推断为最窄的字面量类型

image-20210901171030795.png

值空间与类型空间

image-20210901171509632.png

所谓的类型空间是编译期存在的各种类型,这个空间是编译器tsc创建的

值空间是js的V8引擎在运行的时候创建的,里面存在了各种类型的值

只包含类型声明的namespace不会产生JS代码,不会引入变量

instanceof操作符只作用于值空间

interface IPoint {
    x:number;
    y:number
}
​
const p :IPoint = {x:0,Y:0}
​
// 
if(p instanceof IPoint){
    // 这里if的判断会报错,因为IPoint在上面只有类型没有值
}
​
//如果使用class声明,就不会报错,因为class会同时赋予类型和值
class Point implements IPoint {
    constructor(
        public x :number,
        public y :number
    ){}
}
if(p indetanceof IPoint){
    
}

如何判断符号是在哪个空间?

  1. 转译后消失的符号 -> 类型空间
  2. 作为类型注解.别名的符号 -> 类型空间
  3. 类型断言后的符号 -> 类型空间(target as/is HTMLElement)
  4. const let var后面的符号 -> 值空间
  5. class enum namespace 后的符号 -> 值空间+类型空间

另外,有一些操作符在两个空间都存在,但是含义完全不同

typeof

在值空间,typeof后面表达式对应的Javascript类型的字符串表示:

('string','number','bigint','boolean','symbol','undefined','object','function')

在类型空间,typeof返回标识符对应的TypeScript类型

class Person()
​
const P = typeof Person
console.log(p)  // "function"
const person = new Person()
​
type T = typeof person
//type T = person
​
​
​
//转译成js代码为
"use strict"
class person{
    
}
const p = typeof person
console.log(p) // "function"
const person = new person()
  • this关键字

    在值空间,this指向比较复杂

    在类型空间,this可以作为类方法的返回值来实现链式调用

  • & | 运算符

    在值空间表示"按位与" 和 "按位或"

    在类型空间表示类型的交叉和联合

  • const

    在值空间用来声明常量

    在类型空间与as连用 即as const 常量断言 收窄类型

  • extends

    在值空间用于定义子类

    在类型空间用来进行类型约束或者接口继承

  • in

    在值空间用于for循环和判断属性是否存在

    在类型空间用于映射类型的key的声明

基础知识

TS的类型层次

image-20210901191836184.png

划重点:下层类型的值可以赋给上层类型的变/常量

因为 unknown类型的变/常量可以只想任何类型的值

所以 不存在never类型的变量(never是空集)

不同语言的Top Type和Botton Type

语言Top TypeBottom Type
TypeScriptunknownever
PHPmixednever
Pythonobjecttyping.NoReturn
KotlinAny?Nothing
ScalaAny?Nothing
Javajava.lang.Object
C#System.Object
Gointerface{}
PerlUniversal
C++std::any
Cvoid
Object PascalTObject

any

1.any比较特殊,其实它即是Top Type 又是 Botton Type

也就是说:any类型的变量/常量<->任何其他类型的变量常量 可以互相赋值

但any类型是不安全的 无语言服务的 所以应该尽量避免使用

2.any具有传染性:它会使它所触及的地方变得不安全

所以TS在3.0引入了类型安全的unknown类型作为Top Type

3.any会隐藏bug

因为没有类型信息,即便错误使用,也没有任何报错

比如字符串birthDate没有getTime()方法,但不会报错

4.any会隐藏代码设计细节:丢失了对数据类型的设计

unknown

let a:unknown = 30 //typeof a ==unknown
//unknown 类型必须显示注解  TS不会吧任何值推导为unknown
​
​
let b1 = a > 123 // Error TS2571:Object is of type 'unknown'
let b2 = a < 123 //Error TS2571
let b3 = a ===123 // typeof b == boolean
                    // unknown类型只能进行等于和不等于比较let c = a + 10 // Error TS2571if(typeof a ==='number'){
    let d = a + 10  //typeof d == number
                    // 只有类型收窄后才能进行相应的运算或函数调用
}

如果无法预知类型,不要使用any 用unknown 收窄类型之后再使用

布尔类型

布尔类型只有两个元素 true和false

let a = true //typeof a = boolean
var b = false // typeof b = boolean
const c = true //typeof c = true//let var 变量会被拓展成 Boolean类型  const常量就是对应的字面量类型
​
​
let d: boolean = true //typeof d = boolean
let e: boolean = true //typeof e = boolean//Error TS2322:Type 'false' is not assignable to type 'true'
let f :true = falselet g: true | false = true    //let g :boolean
//有一点值得注意的是,true和false的联合类型,会被反推回boolean类型
​
​

number类型

number包括:整数,浮点数,+_Infinity(正负无穷),NaN

let a = 1234     //typeof a = number
var b = Infinity * 0.1   //typeof b = number
const PI = 3.14   //typeof PI = 3.14
const nan = NaN   //typeof nan = number      虽然叫Not a number但是类型仍然是numberlet c: number = 2.34   //typeof c = number
let d: 4.56 = 4.56     //typeof d = 4.56//Error TS2322:Type '10' is not assignable to type '4.56'
let e: 4.56 = 10

bigint类型

bigint是新引入的类型,可以表示任意大小的整数,number范围[-(2^53 - 1),2^53 - 1)]

bigint字面量是在数字后面加小写n,bigint支持加减乘除,求余 幂

由于bigint类型比较新,我们需要在tsconfig文件中将"target"属性设置为大于ES2020

let a = 1234n  // typeof a = bigint
    
const b 5678n  // typeof b = 5678n
var c = a + b  
const hugeString = BigInt("900789739482")  //typeof hugeString = bigint
const hegeHex = BigInt("0*1fffffffffff")  // typeof hugeHex = bigintconst rounded = 5n/2n  // typeof rounded = bigint  rounded = 2n//Error : Operator '+' cannot be applied to types 'bigint' and '123'
let d = a + 123     // bigint 不能和number混合运算 需要显示转换
let e = a + BigInt(123)
let f = Number(a) + 123

字符串类型String

let a = "hello"   // typeof a = string
let b = "world"   // typeof b = string
const c = "!"		// typeof c = "!"

type Dir = "north" | "south" | "east" | "west"
type Direction = Dir | Capitaliza<Dir> | Uppercase<Dir>
//联合
// type Direction = Dir | "North" | "South" | "East" | "West" |"NORTH" | "SOUTH" | "EAST" | "WEST"

function echoDir(dir: Direction) {consolo.log(dir)}
//dir必须是Direction类型

echoDir('north')
echoDir('NORTH')
echoDir('North')
echoDir('NoRth')  // Error


type RT = XMLHttpRequestResponseType
//字符串字面量联合
// 常用区分数据类型
//type XMLHttpRequestResponseType = ""  | "arraybuffer" | "blob" | "document" | "json" | "text"

符号类型(symbol)

import { Equal } form '@type-challenges/unils'

let a = Symbol('a')  // typeof a = symbol
var a1 = Symbol('a') // typeof a1 = symbol
//let var 声明的变量推导成symbol类型



let a2 = unique symbol = a1 //Error
//unique symbol必须是const常量

const b1x = Symbol('b') //"unique symbol b1x"
// const 常量推导成unique symbol
//也可以显示注解成unique symbol

type X = Equal<typeof b1, typeof b1x>  // false
               //划重点     unique symbol不是一个类型  而是一组类型  比如unique symbol b1 和unique symbol b1x 是两个类型

image-20210901200759429.png

image-20210901200950037.png

对象类型(Object)

image-20210901201002174.png

数组类型(array)

数组有两种注解方式 :1.T[] 2.Array T 接口泛型

let a = [1,2,3]  //typeof a = number[]
var b = ['a','b'] // typeof b = string[]
let c = [1.'a']    // typeof c = (string | number)[]
const d = [2,'b']  // typeof d = (string | number)[]
                    //const 数组不会收窄   因为收窄就变成tuple类型了
​
a.push('red')   // Error :Argument of type 'string' is not assignable
                // to parameter of type 'number'
let e = []      // typeof e = any[]   Ts无法判断空数组的类型  只能推断为any[]
e.push(1);
e.push('red')
​
function buildArray(){
    let e = [];  // typeof e = any []
    e.push(1);
    e.push('red')   // let e:(string | number)[]     但是当离开作用域时 TS可以分析代码   推断出数组的类型
    
    return e
    
}
​
const myarray = buildArray()   
​
​
myarray.push(true)              //Error: Argument of type 'boolean' is not
                                // assignable to parameter of type 'string|number'
myarray.push(2)         //ok
myarray.push('bule')    // ok

元组(Tuple)

元组是数组的子类型,元组各索引上的元素类型是确定的

知识点: 因为元组的创建方法和数组是一样的 所以元素必须显示注解

let a : [number] = [1] 
let b : [string,string,number] = ['a','b',1]
​
b = ['c','d','e']   // Error :Type 'string' is not addignable to type 'number'   因为e的类型应该是数字let c:[number,number?][] [  //支持可选操作符
    [1,2],
    [3.4,5.6],
    [7]
]
type C = typeof c[0] // type C = [number,number?]
​
type StringTuple = [string,...string[]]   //元组支持rest操作符
let d :StringTuple = ['a','b','c'];
​
type NBS = [number,boolean,...string[]]
let list: NBS = [1,false,'a','b','c']
​

[...string[]] 等价于string[],但[string,...string[]]不等价于string[],前者至少包含一个元素

枚举(Enum)

image-20210901202817261.png

image-20210901202832576.png

常量枚举

常量枚举不会在值空间创建变量

所有引用常量枚举的地方都被替换为对应的值

(但是可以通过perserveConstEnum编译器选项来控制)

{
    "compilerOptions" :{
        "preserveConstEnum" : true
    }
}
const enum CJK {
    Chinese,
    Japanese = 'JP',
    Koarean = 1
}
​
console.log(CJK.Chinese)
console.log(CJK.Japanese)
console.log(CJK[0])  // Error  
console.log(CJK)     // Error     常量枚举没有双向映射const enum Colors { Red,Green,Blue}
​
const enum Colors {
    Orange = 3,Yellow,Purple
    //常量枚举仍然支持多分段   但是由于常量枚举不生成变量  所以不支持与namespace的合并
}
​
console.log(Colors.Blur)
​
​
​
​
//js代码
"use strict"
console.log(0 /* Chinese */)
console.log("JP" /* Japanese */)   //TS会输出常量对应的Key  方便调试
console.log(CJK[0])   // Error
console.log(CJK)   //Error
console.log(2 /* Blue */)

null undefined void never

JS中 Undefined = {undefined} 应该表示尚未定义 但是 实际上已声明 未赋值 Null = {null} 表示已声明 值为null或值为空

TS中void类型:函数没有显式返回值 never类型 函数无法返回