ts是什么?the shy?不,ts是由微软公司研发,是js的超集。众所周知,js是弱类型的语言,变量在声明时,不需要说明类型,变量可以赋予任何值。这样写起来非常方便,但是也带来了一些安全隐患,比如涉及到运算时,就会出现一些问题。另外,js是解释型(即时编译型)语言,代码不会进行预编译,所以我们在编写时,可能不会出现问题,但运行时就会bug满天飞。使用ts可以大大减少上述问题,提高项目的可维护性。
一、环境配置
1. 全局安装 typescript
命令:npm i typescript -g
2. 初始化
命令:tsc --init
此命令会生成一个 tsconfig.json 文件,里面包含了对ts编译器的配置
3. ts 编译为 js
命令:tsc 文件名(如果不写就默认编译目录中所有的ts文件)
4. 自动编译
命令:tsc -w
-w 表示watch,文件保存时就会自动编译
二、学习时的问题
在vscode中,会默认将当前打开的ts文件看作一个全局作用域,所以变量名相同时,会报错不过这里我们只是学习基础知识,在后面的正式项目中不会有这个问题。
解决方案:
可以使用,import/export,将ts文件改为文件模块,此时就会在当前文件创建本地作用域
比如在文件开头写:export const qqq = 1
三、tsconfig.json ts文件编译配置
1. include 指定编译那些目录下的 ts 文件
路径:** 表示任意目录
* 表示任意文件
示例:
"include": [
"./code/**/*"
],
2. exclude 指定哪些目录下的 ts 文件不需要编译
示例:
"exclude": [
"./code/01_数据类型"
],
3. compilerOptions
"compilerOptions": {
// target 指定 ts 编译为哪个版本的 es
"target": "es2016",
// module 指定 使用哪个模块化规范 ES6 | common.js
"module": "commonjs",
// lib 用来指定项目中要用到哪些库
"lib": ["es6", "dom"],
// outDir 指定编译后文件所在的目录
"outDir": "./dist",
// outFile 将代码合并为一个文件,设置 outFile 后,所有的全局作用域中的代码会合并到同一个文件中
"outFile": "./dist/app.js",
// allowJs 是否对 js 文件进行编译,默认为false
"allowJs": false,
// checkJs 是否检查 js 文件语法规范,默认为false
"checkJs": false,
// removeComments 是否删除注释,默认为false
"removeComments": false,
// noEmit 不生成编译后的文件,默认为false
"noEmit": false,
// noEmitOnError 当有错误时,不生成编译后的文件,默认为false
"noEmitOnError": false,
// strict 所有严格检查的总开关,如果为true,则以下默认均为true,默认为true
"strict": true,
// alwaysStrict 设置编译后的 js 文件是否为严格模式,默认为true
"alwaysStrict": false,
// noImplicitAny 不允许出现隐式的 any 类型,默认为true
"noImplicitAny": false,
// noImplicitThis 不允许出现不明确类型的 this,默认为true
"noImplicitThis": false,
// noImplicitThis 不允许出现不明确类型的 this,默认为true
"noImplicitThis": false,
// strictNullChecks 严格的检查空值,默认为true
"strictNullChecks": false
}
四、基本类型声明
ts中的基本类型与js中的对应,有 number, boolean, string, undefined, null, symbol, bigint(target版本高于ES2020),也新增了 any, unknown类型,语法如下:
let b: string // 注意是小写
b = '123'
// 使用字面量进行类型声明,变量只能等于声明的字面量
let a: 10;
a = 10;
any表示任意类型,相当于关闭了对该变量的类型检查。如果声明时不指定类型,TS默认会判断变量类型为any, any可以赋为任何值,也可以被赋予任何变量。不推荐使用any类型。
let d:any;
d = 1;
d = '1'
d = true
let f;
f = 1; f='123'
unknown 表示未知类型的值(类型安全的 any),unknown可以被赋值为任何值
let e: unknown;
e = 10;
e = '123';
e = true;
e = f
let s:string = '123'
e = s
五、引用类型声明
1.对象类型声明, object, Object, {}, {属性名: 类型}
// Object 大写O,因为js原型链顶级对象是Object,所以Object可以是任意类型,与 any 类似
let obj: Object = '123'
obj = 123
obj = () => 123
obj = {a:1}
// object 小写o, 表示类型为非原始类型(数组,对象,函数)
let a: object;
a = {}
a = function(){}
// {}, 与大写 Object 类似,可以是任意类型
let obj2:{} = 123
obj2 = '123'
obj2 = {name:'z'}
obj2 = [1,2,3]
obj2 = () => 123
// 字面量形式,{} 中指定对象中包含哪些属性
// 如果一个属性是可选的,可以在属性名后加?, age?: number
let b: {name: string, age?: number}
b = {name: 'qwer'}
// [propName: string]: any 表示任意属性,当后端返回的数据中,其他属性是未知的,可以这样来接收
let c: {name: string, [propName: string]: any}
c = {name: 'www', a: 1, b: 2}
2. 数组类型声明
语法: 类型[] 或 Array<类型>
/**
* 数组类型的声明:
* 类型[]
* Array<类型>
*/
let arr : Array<number>
arr = [1, 2, 3]
let e: string[];
e = ['a','b','c']
// 接口定义数组
interface X{
name: string
}
let arr2: X[] = [{name:'坤'}, {name: '鸡'}]
// 二维数组
let arr3: number[][] = [[1],[2],[4]]
let arr4: Array<Array<number>> = [[1],[2],[3]]
3. 函数类型声明
语法:(形参:类型, 形参:类型...) => 返回值类型
返回值类型除了常见的数据类型外,添加了 void 和 never 类型:
void 表示空,表示函数没有返回值(没有return 语句或返回为 undefined)
never 表示永远不会返回结果,包括undefined
/**
* 设置函数结构的类型声明:
* 语法:(形参:类型, 形参:类型...) => 返回值类型
*/
let d: (a:number, b:number) => number;
d = function(n1: number, n2: number): number{
return n1 + n2
}
// void 表示空,表示函数没有返回值(没有return 语句或返回为 undefined)
function fn1(): void{}
// never 表示永远不会返回结果,包括undefined
// 通常用于捕获错误
function fn2(): never{
throw new Error('出错啦')
}
对于函数的参数对象 arguments ,ts内置有 IArguments 类型,此类型原理是用 interface实现的。
// 函数参数
function arr5(...args: string[]){
console.log(args);
// 对于 arguments 对象,ts内置有 IArguments 类型
// IArguments 的原理使用 interface 实现的
interface A{
callee: Function
length: number
[index:number]:any
}
// let a:IArguments = arguments
let a:A = arguments
}
arr5('1','2')
4. 元组类型声明
ts中,新增了元组类型,表示固定长度的数组。
语法: 变量名: [类型, 类型...]
let f: [string, number];
f = ['123', 123]
5. 枚举类型声明
ts 新增了枚举类型。语法:
enum Gender{
Male = 0,
Female = 1
}
let g: {name: string, gender: Gender};
g = {name: '奥里给', gender: Gender.Male}
// 枚举默认值是从0开始递增的,类似数组索引
enum Color{
red,
blue,
green
}
console.log(Color.red) // 0
console.log(Color.blue) // 1
console.log(Color.green) // 2
// 枚举可以进行反向映射
let male = Gender.Male
console.log('key:', Gender[male]) // key: Male
console.log('value:', male) // value: 0
6. this执行环境上下文类型声明
ts 可以定义 this 的类型,但在js中无法使用,且必须在第一个参数定义
interface Obj{
user:number[]
add:(this:Obj, num:number) => void
}
let newObj:Obj = {
user:[1,2,3],
// ts 可以定义 this 的类型, 但在js中无法使用,且必须在第一个参数定义
add(this:Obj, num:number){
this.user.push(num)
console.log(this.user);
}
}
newObj.add(4)
六、交叉类型、联合类型、类型断言
1. 交叉类型
声明时,类型用 & 隔开,赋值时,类型需要都符合,类似 与。
// 交叉类型, 用 & 连接
let w : {name:string} & {age: number}
w = {name:'张三',age:18}
2. 联合类型
声明时,类型用 | 隔开,需要符合其中一个类型,类似 或。
// 联合类型 用 | 连接
let phone: number | string = '13939178797'
3. 类型断言
类型断言,可以用来告诉解析器变量的实际类型,语法:
写法一:属性名 as 类型
写法二:<类型>属性名
类型断言并不会真的实现类型转换,只是欺骗一下ts
let g = 123;
g = s as any;
g = <any>s
let fn = function(num:number | string):void {
// num是数字,所以没有length属性
console.log((num as string).length); // undefined
console.log((<string>num).length); // 写法2
}
fn(12345)
let fn2 = (type:any):boolean => {
return type as boolean
}
console.log(fn2(1)); // 1
4.类型别名
使用 type 关键字定义类型别名,语法:
type S = 1 | 2 | string
let a:S = '333'
// 高级用法
type num = 1 extends Number ? 1 : 0
// 上面 extends 表示包含
// num 的类型是number 被Number包含 所以返回的是1
// 优先级自上而下:
/**
unknown any
Object
Number
number string boolean ....
never
*/
七、interface 接口
接口重名时,会将里面的属性重合
interface Ikun{
hair: string
}
interface Ikun{
say: string
}
let kun: Ikun = {
hair:'中分',
say: '你干嘛'
}
任意key,语法:[propName:string]:any,当后端返回的数据中,其他属性是未知的,可以这样来接收
interface Ikun{
name: string
[propName: string]:any
}
let kun: Ikun = {
name: 'kun',
hobby1: '唱',
hobby2: '跳',
hobby3: 'Rap',
hobby4: '篮球'
}
?可选属性 与 readonly只读属性。? 表示 该属性可有可无,readonly表示该属性为只读,不可修改,比如后台返回的id
interface Ikun{
readonly id: number
name: string
age?: number
}
let kun: Ikun = {
id: 1
name: 'kun'
}
接口继承 与类的继承相同,使用extends关键字
interface Chicken{
hair: string
}
interface Ikun extends Chicken{
name: string
age: number
}
let kun: Ikun = {
hair:'中分',
name: 'kun',
age: 18
}
定义函数类型,语法: (参数名:类型): 返回值类型
interface Fn {
(name:string): number[]
}
const fn:Fn = function(name:string){
return [1]
}
八、泛型
什么是泛型?泛型就是动态类型。泛型相当于占位符,我们定义时使用泛型,使用时ts就会根据我们传入的数据,自动推断出类型,将泛型替换。非常灵活。示例:
// 泛型通常用大写 T 表示
function fn<T>(a:T, b:T): Array<T>{
return [a, b]
}
fn(1,2)
// 类型别名
type A<T> = string | number | T
let a:A<null> = null
// 接口中使用
interface Data<T> {
msg: T
}
let data: Data<string> = {
msg:'hhh'
}
// 多个泛型
function add<T,K>(a:T, b:K): Array<T | K>{
return [a, b]
}
add(false, '1')
由于泛型过于灵活,也会带来一些问题,比如定义一个相加函数,传入的数据必须是可以进行计算的,所以要对传入的数据进行约束,可以使用 extends,示例:
// 语法:<T extends 约束的类型>
function sum<T extends number>(a:T,b:T){
return a + b
}
sum(1, 2)
// 对属性进行约束
interface Len {
length: number
}
function fn2<T extends Len>(a:T){
console.log(a.length);
}
fn2('123')
fn2([1,2,3])
使用 keyof ,约束对象中的key:
let obj = {
name: '马保国',
hobby: '五连鞭'
}
function ob<T extends object, K extends keyof T>(obj:T, key:K){
return obj[key]
}
ob(obj, 'name') // 只能传入 'name' 和 'hobby'
keyof 的高级用法,将所有的key都变为可选,类似于 for...in...
type Options<T extends object> = {
[key in keyof T]? : T[key]
// readonly [key in keyof T] ? : T[key] // 同样的,将所有属性变为只读
}
interface Person{
name: string
age: number
sex: string
}
type P = Options<Person>
let person: P = {}
九、namespace命名空间
为了防止对全局变量造成污染,ts提供了命名空间
通过 namespace 关键字定义
通过 export 暴漏
命名空间内的类默认私有
ts在编译时会将命名空间编译为一个函数
namespace A {
export const a = 1
}
console.log(A.a);
// 嵌套命名空间
namespace B{
export namespace C{
export const d = 1
}
}
console.log(B.C.d);
// 嵌套过深时,可以用import进行重命名
import AAA = B.C
console.log(AAA.d);
十、mixins 混入
对象的混入,使用 Object.assign(),混入后,对象的类型会变为联合类型:
interface Name {
name: string
}
interface Age {
age: number
}
interface Sex {
sex: string
}
let a: Name = {name:'张三'}
let b: Age = {age: 18}
let c: Sex = {sex:'男'}
// 对象的混入,Object.assign() obj类型为 Name & Age & Sex
let obj = Object.assign(a,b,c)
类的混入,使用 implements 关键字,将混入类的原型,赋给目标类:
class A {
type: boolean
changeType(): void{
this.type = !this.type
}
}
class B {
name: string
getName(): string {
return this.name
}
}
class C implements A,B {
type: boolean = false
name: string = '张三'
changeType: () => void
getName: () => string
}
function mixins (curClass: any, itemClass:any[]) {
itemClass.forEach(item => {
Object.getOwnPropertyNames(item.prototype).forEach(name => {
curClass.prototype[name] = item.prototype[name]
})
})
}
mixins(C, [A,B])
let ccc = new C();
ccc.changeType()
console.log(ccc.type, ccc.getName()); // true 张三
十一、Decorator 装饰器
装饰器(Decorators)的作用是为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。(注意: 装饰器是一项实验性特性,在未来的版本中可能会发生改变。)装饰器的本质是一个函数,使用前需要修改tsconfig.json配置文件:
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
}
1. 类装饰器 ClassDecorator
语法:
@装饰器名 class 类名 {}
示例:
// target 是 这个类本身,也就是构造函数 A.prototype.constructor === A
// 如果想在类中添加一些属性,但是类中的属性很多,我们可以直接在类的原型中添加
const Base:ClassDecorator = (target) => {
console.log(target)
target.prototype.name = 'kunkun'
target.prototype.fn = () => {
console.log('唱,跳,rap,篮球');
}
}
@Base class A {}
const a = new A() as any
console.log(a.name); // kunkun
a.fn() // 唱,跳,rap,篮球
上面例子中,装饰器默认接收一个参数,这个参数指向这个类,也就是构造函数(ES6中的class是构造函数的一个语法糖,类本身就指向构造函数)。如果想在类中添加一些属性,但是类中的属性很多,我们可以直接在类的原型中添加。
装饰器工厂:
如果需要传入额外参数,可以使用函数柯里化(闭包):
// 函数柯里化,用于接收额外参数
const Base = (name: string) => {
const fn: ClassDecorator = (target) => {
console.log(target)
target.prototype.name = name
target.prototype.fn = () => {
console.log('唱,跳,rap,篮球');
}
}
return fn
}
@Base('ikun') class A {}
const a = new A() as any
console.log(a.name); // ikun
a.fn() // 唱,跳,rap,篮球
当多个装饰器应用于一个声明上,它们求值方式与复合函数相似。在这个模型下,当复合f和g时,复合的结果(f ∘ g)(x)等同于f(g(x))。
// 单行写法
@f @g class A{}
// 多行写法
@f
@g
class B{}
2.方法装饰器MethodDecorator
如果类中有些方法,需要去处理,我们可以使用方法装饰器,方法装饰器声明在一个方法的声明之前(紧靠着方法声明),语法与类修饰器类似:
const Data = (str:any) => {
const fn:MethodDecorator = (target, key, descriptor) => {
// console.log(target, key, descriptor);
target[key]('我是' + str)
}
return fn
}
class Obj {
@Data('ikun')
getName(data:any){
console.log(data);
}
}
方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
成员的名字。
成员的属性描述符。
除了上述两种装饰器,还有访问器装饰器、属性装饰器、参数装饰器,想要了解的可以查看ts官方文档:
https://www.tslang.cn/docs/handbook/decorators.html#method-decorators