从0到1TypeScript
前言
TS入门教程,从0到1
TS介绍
TS是什么?
ts是js的超集,超集指的是他包含了es前面所有版本的内容并有自己的拓展。
TS做到了什么?
首先js是弱类型,很多错误在运行的时候才能发现。
ts使用了他的静态类型检测机制(后面提及)帮助我们提早发现错误。
TS特点
- 支持最新的js特性(超集)
- 静态代码检查(解决弱类型)
- 有其他后端语言的特性(枚举,泛型,类型转化,命名空间,声明文件…)
静态类型检测
为什么会产生这个问题?
js在运行代码的时候,需要我们人为的知道代码的返回值是什么,才能做出响应的操作(执行函数才能知道)。不能在我们书写的过程就立刻给出编译错误的反馈。
ts就能在(编译之前)告诉我们
非异常故障
ts不仅能告诉我们哪些函数的值错误,还能识别类似于错别字,未调用函数和基本逻辑错误1
2
3
4const user={
name:'ber'
}
user.location
上面这段代码在js中返回undefined
,而在ts中提前告诉你这个变量没有定义不能使用
错别字
:告诉你对象上面某个方法拼写错误等
未调用函数
:函数没有加括号就使用,会提示未调用错误
逻辑错误
:比如只有两种条件的情况下,if一个条件又elseif另外一个条件,就会报错
搭建环境
安装ts
1 | npm i -g typescript |
安装ts-node
可以直接编译ts文件1
2
3npm i -g ts-node
//使用
npx ts-node xxx.ts
创建一个tsconfig.json
解决变量名or函数名相同报错的问题1
tsc --init
官方playground
官方提供的ts在线编译云环境—playground
编译
在对应的目录终端运行1
tsc hello.ts
得到对应的js文件,执行node命令就可以执行js代码
ts和js的冲突
当我们将ts代码修改为一个函数并调用的时候,再次编译运行js文件,会产生冲突
原因:函数名or变量名相同,重复定义
解决方法:
tsc --init
生成配置文件- 注释掉
"strict": true, o9
自动编译
tsc --watch
发出错误
我们在ts里面报了错,但是在编译完之后,还是能运行js文件,如果我们要优化这个过程,结果为:ts编译报错就不生成js文件的话,需要如下:
tsc -noEmitOnError hello.ts
or
tsc -noEmitOnError --watch
降级编译
设置json里面的target为es5
严格模式
不同的用户希望在ts里面检查的严格程度是不一样的,配置如下
strict
是包含了下面两个类型的模式
noImplicitAny
是指不可以忽略any,也就是要定义类型
strictNullChecks
意思是null undefined
只能赋值给对应的类型
基础数据类型
显式类型
ts给我们提供了类型定义的注释,比如说1
2
3
4function conflit(person:string,date:Date){
console.log(`hello,${person},${date}`)
}
conflit('jojo',new Date());
但是在编译成js之后就会删去,可以方便程序员查看。
根据上面显式类型的说法我们知道了ts可以定义很多数据类型,比如1
2
3
4
5
6
7
8let str: string = "jimmy";
let num: number = 24;
let bool: boolean = false;
let u: undefined = undefined;
let n: null = null;
let obj: object = {x: 1};
let big: bigint = 100n;
let sym: symbol = Symbol("me");
几个需要留意的数据
当我们定义好类型的时候,如果给变量赋值别的类型是会报错提示的,但是有两个类型不一样
null&undefined
null undefined
这两个类型可以作为其他所有类型的子类型,也就是可以把它们赋给别的值
1 | // null和undefined赋值给string |
但如果在tsconfig.json
里面指定了"strictNullChecks":true
那么它们只能被赋值给自己的类型或者void
number&bigint
这两个值互不兼容,也就是说不能相互赋值,会报错
Array
对数组的定义有两种方式
1 | //arr:type[] |
定义联合类型数组1
2
3//arr:(type1|type2)[]
let arr3:(number | string)[];
arr3 = ["1","sdcas",1,2];
定义对象类型数组1
2
3
4
5interface Arrobj {
name:string,
age:number
}
let arr4:Arrobj[] = [{name:'jojo',age:18}]
any
定义成any类型的时候表示你不希望某个特定值导致类型错误的产生。
注意避免使用any 这样会丢失ts的保护机制
函数
函数声明
具名函数1
2
3function sum(x: number,y: number): number {
return x+y;
}
这个是告诉我们函数的返回值也可以指定类型,但一般不需要,因为ts会给我们自动判断返回值的类型
匿名函数:会自动根据上下文判断读取的数据类型是什么。
函数表达式
1 | let sum: (x: number, y: number) => number = function (x: number, y: number): number { |
用接口定义函数类型
1 | interface SearchFunc{ |
采用函数表达式接口定义函数类型的数据的时候,对等号左侧进行类型限制,可以保证以后对函数名赋值时保证参数个数,参数类型,返回值类型不变。
可选参数
使用变量名?: type
的形式1
2
3
4
5
6
7
8
9function myFun(x: number,y?: number){
if(y){
return x+y;
}else{
return x;
}
}
console.log(myFun(1,2))
console.log(myFun(33))
注意可选参数后面不能再跟必需参数
参数默认值
在type后面加= 默认值
1
2
3
4
5function myFun(x: number, y: number = 22) {
return x + y;
}
console.log(myFun(1, 2))
console.log(myFun(33))
剩余参数
注意需要用any来指定rest1
2
3
4
5
6
7
8function myFun(arr: Array<number>, ...items: any[]) {
items.forEach((item)=>{
arr.push(item);
})
return arr;
}
let arr: Array<number> = [1,2,3];
console.log(myFun(arr,3,4,543414,123))
函数重载
js是动态语言,会根据不同的参数返回不同类型的调用结果1
2
3
4
5function add(x, y) {
return x + y;
}
add(1, 2); // 3
add("1", "2"); //"12"
but如果开启了noImplicitAny的话,ts不能用上述代码,因为需要指定类型,所以就诞生了联合类型帮助我们根据不同的情况返回不同的结果
1 | type Combination = string | number; |
但其实到这一步,看似没有问题了,实际上新的问题又产生了1
2
3
4
5
6
7
8
9
10
11type Combination = string | number;
function myFun(x: Combination, y: Combination) {
if (typeof x === 'string' || typeof y === 'string') {
return x.toString() + y.toString();
} else {
return x + y;
}
}
const result = myFun("abc","def");
console.log(result);
result.split(" ");//报错
查看提示可以知道number类型上面没有方法split,为什么是number类型呢?因为我们这个参数虽然定义了组合类型,但是我们的ts并不知道返回值是什么类型,所以产生了函数重载
函数重载就是使用相同函数名称和不同参数类型或不同参数个数创建方法的一种能力
1 | type Combination = string | number; |
上述实现了函数的重载
对象
1 | function PrintCoord(pt:{x: number,y: number}){ |
配合可选参数+可选链的例子1
2
3
4function PrintCoord(pt:{x: number,y?: string}){
console.log(pt.y?.toUpperCase())
}
PrintCoord({x:333,y:'jojo'})
接口
我们定义接口可以用于类型的后续拓展,看基础应用如下1
2
3
4
5
6
7
8interface Point {
x: number,
y: number
}
function myFun(pt:Point){
return pt.x+pt.y;
}
console.log(myFun({x:1,y:2}))
拓展接口如下:1
2
3
4
5
6
7
8
9
10
11interface Animal {
name: string
}
interface Bear extends Animal{
honey: string
}
const bear: Bear = {
name:'jobear',
honey:'蜂蜜'
}
console.log(bear)
type的拓展 通过&符号1
2
3
4
5
6
7
8
9
10
11type Animal = {
name: string
}
type Bear = Animal &{
honey: string
}
const bear: Bear = {
name:'jobear',
honey:'蜂蜜'
}
console.log(bear)
向现有的类型添加字段(重复定义接口名字添加)1
2
3
4
5
6
7
8
9
10
11interface jojo {
name: string
}
interface jojo {
age: number
}
const j: jojo = {
name: 'jojo',
age: 18
}
console.log(j)
类型创建后不能更改1
2
3
4
5
6
7type jojo = {
name: string
}
type jojo = {
age: number
}
//报错
void
void表示没有任何类型,一般的用处是函数没有返回值的时候定义为void1
2
3
4function myFunc(): void{
console.log('this is a void function')
}
myFunc()
never
never表示永不存在值的类型
比方说当函数遇到异常的时候,那么这个函数永远都不会有返回值,因为抛出异常直接中断了程序运行,不会到最后一步。
或者说代码中遇到无限循环的时候,程序永远到不了执行函数返回值的那一步,永不存在返回
使用的场景是:
针对于我们的代码来说,如果我们定义的联合类型被修改了,那么会因为我们事先在条件判断中写了never从而报错,因此在代码的安全性来讲never是比较可靠的使用。1
2
3
4
5
6
7
8
9
10
11
12
13type Foo = string | number;
function controlFlowAnalysisWithNever(foo: Foo) {
if (typeof foo === "string") {
// 这里 foo 被收窄为 string 类型
} else if (typeof foo === "number") {
// 这里 foo 被收窄为 number 类型
} else {
// foo 在这里是 never
const check: never = foo;
}
}
//如果有人修改了type的类型 将报错
unknown
unknown定义的类型和any一样可以赋全部值
不要弄混原始类型和对象类型
我们在初学ts的时候,很容易把number,boolean这一类和Number Boolean
搞混,实际上前者属于原始类型,后者属于原始类型的包装对象,在使用上面,前者可以赋值给后者,后者不能赋值给前者
1 | let num: number; |
元组
元组用来限制数组元素的个数和类型,是ts独有的机制,适合用于多值返回
元组定义的数据必须类型和个数都要匹配1
2
3
4
5
6let x: [string, number];
// 类型必须匹配且个数必须为2
x = ['hello', 10]; // OK
x = ['hello', 10,10]; // Error
x = [10, 'hello']; // Error
元组的解构赋值
元组也是支持解构赋值的
1 | let employee: [number, string] = [1, "Semlinker"]; |
元组类型的可选元素
见例子知用法1
2
3
4
5
6
7
8
9type Point = [number, number?, number?];
const x: Point = [10]; // 一维坐标点
const xy: Point = [10, 20]; // 二维坐标点
const xyz: Point = [10, 20, 10]; // 三维坐标点
console.log(x.length); // 1
console.log(xy.length); // 2
console.log(xyz.length); // 3
元组的剩余元素
1 | type RestTupleType = [number, ...string[]]; |
元组的只读类型
1 | const point: readonly [number, number] = [10, 20]; |
类型断言
ts提供了as xxx进行类型的断言,这样子我们在自身清楚实际的类型的情况下,让编译器不要干扰你的类型值,就好比any,但是使用了any就完全指定不了了,下面是一个例子1
2
3const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find(num => num > 2) as number;
console.log(greaterThan2)
告诉编译器我要编译出来的结果是什么类型
语法如下:两种类型1
2
3
4
5
6
7// 尖括号 语法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
// as 语法
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
尖括号语法会和react中的JSX冲突,所以更推荐使用as
非空断言
在上下文中ts的类型检查器无法判断类型的时候,使用!
就可以用于断言操作对象是非null和非undefined类型
举例,x!将从x的值域排除null和undefined
eg1
2
3
4
5
6
7
8type NumGenerator = () => number;
function myFunc(numGenerator: NumGenerator | undefined) {
// Object is possibly 'undefined'.(2532)
// Cannot invoke an object which is possibly 'undefined'.(2722)
const num1 = numGenerator(); // Error
const num2 = numGenerator!(); //OK
}
确定赋值断言
在变量声明后面使用!
这个断言,告诉ts这个变量会被明确的赋值
1 | let x!: number; |
如果不写该断言,则会报错为该数在赋值前被使用了。
枚举
ts独有的定义方式,js中没有,通过enum来进行定义
枚举会自动帮我们递增1
2
3
4
5
6
7
8
9
10
11enum Direaction{
UP=1,
DOWN,
LEFT,
RIGHT
}
console.log(Direaction.UP);
console.log(Direaction.RIGHT);
console.log(Direaction.DOWN);
console.log(Direaction.LEFT);
//1 4 2 3
场景:我们知道数值,但不知道它映射到的枚举类型的哪个名字,就可以使用1
2
3
4enum Color {Red = 1, Green, Blue}
let colorName: string = Color[2];
alert(colorName); // 显示'Green'因为上面代码里它的值是2
类型拓宽
通过ts推断出来的类型就是字面量拓宽
1 | let str = 'this is string'; // 类型是 string |
类型缩小
将类型声明细化为更具体的类型的过程我们称之为类型缩小
1 | function padLeft(padding: number | string, input: string): string { |
真值缩小
使用条件、&&、||
、if语句,布尔否定!
进行缩小的方式称为真值缩小,常见如下:
- 强制类型转换
- 使用Boolean
- 使用
!!
,第一个叹号将文字转换为布尔类型,再通过另外一个叹号转成真正的true或者false
等值缩小
ts可以通过真值判断类型,如下
1 | function myFunc(x: number|string, y:boolean|string){ |
in缩小
js的in操作符用于确定某个对象是否拥有该名称的属性
instanceof缩小
略
分配缩小
类型谓词
字面量类型
ts支持三种字面量类型:字符串字面量、数字字面量、布尔字面量
1 | { |
联合类型
使用|符号
1 | let myFavoriteNumber: string | number; |
交叉类型
和联合类型差不多,不过这里的符号用的是&,可以理解为是所有类型都要有,类似于求并集
1 | type IntersectionType = { id: number; name: string; } & { age: number }; |
这里引申出一个问题,如果合并的是同名的属性类型会怎么样呢?
情况1 两个同名key 但是类型不同1
2
3
4
5
6
7type IntersectionTypeConfict = { id: number; name: string; }
& { age: number; name: number; };
const mixedConflict: IntersectionTypeConfict = {
id: 1,
name: 2, // ts(2322) 错误,'number' 类型不能赋给 'never' 类型
age: 2
};
这种情况下直接pass
情况2 两个同名key 类型相同, 但其中一个是另一个是子类型1
2
3
4
5
6
7
8
9
10
11
12
13
14type IntersectionTypeConfict = { id: number; name: 2; }
& { age: number; name: number; };
let mixedConflict: IntersectionTypeConfict = {
id: 1,
name: 2, // ok
age: 2
};
mixedConflict = {
id: 1,
name: 22, // '22' 类型不能赋给 '2' 类型
age: 2
};
这种情况以子类型为主
情况3 两个同名key 类型不是基本类型1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17interface A {
x:{d:true},
}
interface B {
x:{e:string},
}
interface C {
x:{f:number},
}
type ABC = A & B & C
let abc:ABC = {
x:{
d:true,
e:'',
f:666
}
}
结果是可以合并
泛型函数
泛型可以帮助我们关联参数的值和返回值之间的关系,见下面的例子。
需求:如果我们要实现一个函数 identity,函数的参数可以是任何值,返回值就是将参数原样返回,并且其只能接受一个参数,你会怎么做?
由于其可以接受任意值,也就是说你的函数的入参和返回值都应该可以是任意类型。 现在让我们给代码增加类型声明:1
2
3
4
5type idBoolean = (arg: boolean) => boolean;
type idNumber = (arg: number) => number;
type idString = (arg: string) => string;
...
这样下来你每次都要写无穷无尽的代码,很明显是不太合理的,所以引出了泛型这个概念,下面看看泛型的用法:1
2
3function identity<T>(arg: T): T {
return arg;
}
其中T是一个抽象类型type,只有在调用它的时候才确定它的值。
除了T以外,还有其他的泛型代表:
- K key 表示对象中的键类型
- V value 表示对象中的值类型
- E element 表示元素类型
其实并不是只能定义一个类型变量,我们可以引入自定义的类型变量如下1
2
3
4
5
6function identity <T, U>(value: T, message: U) : T {
console.log(message);
return value;
}
console.log(identity<Number, string>(68, "Semlinker"));
也可以简略的调用,编译器会帮我们自动识别。1
console.log(identity(68, "Semlinker"));
泛型约束
如果想打印出参数的size,直接使用是不行的,因为T只是类型,并不是所有方法的集合
1 | function trace<T>(arg: T): T { |
如何操作使得他能拥有这个size类型呢?其实很简单,只要你自己定义一个接口,让T去继承即可1
2
3
4
5
6
7interface Sizeable{
size:number
}
function trace<T extends Sizeable>(args:T): T{
console.log(args.size);
return args
}
泛型工具类型
为了方便开发者,ts设置了一些工具类比如Partial、Required、Readonly等。但我们需要知道一些基础知识方便我们后续用这些工具类
typeof
关于typeof我们知道是用于判断类型的,但你可能不会做下面这样的事情
1 | interface Person{ |
在上述代码用了typeof获取了变量的类型并赋给了JOJO,后面我们就可以用到JOJO类型
除此之外,typeof还可以用来获取函数的类型、对象的结构类型1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23const Message = {
name: "jimmy",
age: 18,
address: {
province: '四川',
city: '成都'
}
}
type message = typeof Message;
/*
type message = {
name: string;
age: number;
address: {
province: string;
city: string;
};
}
*/
function toArray(x: number): Array<number> {
return [x];
}
type Func = typeof toArray; // -> (x: number) => number[]
keyof
该方法返回类型的所有键,返回值是联合类型
1 | interface Person { |
keyof的作用
js是一门高度动态的语言,所以有些时候在我们的ts里面想要捕获到某些操作可能很麻烦,比如
1 | function prop(obj:object,key:string){ |
元素隐式的拥有any类型,所以string类型不能用于索引{}类型,因此我们需要利用到keyof和泛型来帮助我们解决这个问题
1 | function prop<T extend object,K extends keyof T>(obj: T,key: K){ |
在上述的代码中,T约束了object,K约束了T的所有键,所以非常的合理,此时访问不存在的属性的时候,也会提示错误了。
in(具体场景?)
in用来遍历枚举类型
1 | type Keys = "a" | "b" | "c" |
infer
infer就是取到函数值的返回类型,方便后续使用
1 | type ReturnType<T> = T extends ( |
extends
添加约束,上面已经讲到了用法和场景,略
索引类型
在实际开发中,我们经常需要在对象中获取一些值然后建立对应的集合
1 | let person = { |
但此时ts并没有对不存在的值进行报错,通过这里我们想到了keyof+extends,结合我们的索引类型操作符[]
1 | function getValues<T, K extends keyof T>(person: T, keys: K[]): T[K][] { |
上述代码中,T[K]
表示对象T的属性K所表示的类型,那么T[K][]
就是对象T的属性K所表示类型的数组。
映射类型
根据旧的类型创造出新的类型,称之为映射类型
映射类型的用法非常巧妙,如下例子将接口的所有属性变成可选1
2
3
4
5
6
7
8
9
10
11
12
13
14
15interface TestInterface{
name:string,
age:number
}
//希望将接口的所有属性变成可选
//在ts中可以通过+/-添加或者删除
type OptionTestInterface<T> = {
[p in keyof T]+?:T[p]
}
type newTestInterface = OptionTestInterface<TestInterface>
// type newTestInterface = {
// name?:string,
// age?:number
// }
将接口的所有属性变成只读1
2
3
4
5
6
7
8
9
10
11
type OptionalTestInterface<T> = {
+readonly [p in keyof T]+?:T[p]
}
type newTestInterface = OptionalTestInterface<TestInterface>
// type newTestInterface = {
// readonly name?:string,
// readonly age?:number
// }
由于生成只读和可选属性比较常用,ts已经内置了Readonly和Partial工具类型
Partial 可选
我们看例子1
2
3
4
5
6
7
8
9
10
11interface UserInfo{
id: string,
name: string
}
//变成可选
type newUserInfo = Partial<UserInfo>;
//interface newUserInfo {
// id?: string;
// name?: string;
//}
Partial有局限性,他只负责第一层的数据,如果多层就要自己实现了。
DeepPartial 深度可选
1 | type DeepPartial<T> = { |
Required 必选
将所有属性变成必选
1 | type Required<T> = { |
其中-?
是为了移除可选
Readonly 只读
只读,不可修改
原理1
2
3type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
eg1
2
3
4
5
6
7
8
9interface Todo {
title: string;
}
const todo: Readonly<Todo> = {
title: "Delete inactive users"
};
todo.title = "Hello"; // Error: cannot reassign a readonly property
Pick 挑选属性
原理1
2
3type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
例子1
2
3
4
5
6
7
8
9
10
11
12interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Pick<Todo, "title" | "completed">;
//TodoPreview 只有title和completed了
const todo: TodoPreview = {
title: "Clean room",
completed: false,
};
Record
将K中所有属性值转换为T类型
原理1
2
3type Record<K extends keyof any, T> = {
[P in K]: T;
};
例子1
2
3
4
5
6
7
8
9
10
11interface PageInfo {
title: string;
}
type Page = "home" | "about" | "contact";
const x: Record<Page, PageInfo> = {
about: { title: "about" },
contact: { title: "contact" },
home: { title: "home" },
};
ReturnType 获取返回类型
得到函数的返回值类型
原理1
2
3
4
5type ReturnType<T extends (...args: any[]) => any> = T extends (
...args: any[]
) => infer R
? R
: any;
例子1
2type Func = (value: number) => string;
const foo: ReturnType<Func> = "1";ReturnType
获取到Func的类型是字符串,所以foo也只能被赋为字符串了
Exclude 移除
将某个类型中属于另外一个的类型移除,类似于类型去重
原理1
type Exclude<T, U> = T extends U ? never : T;
例子1
2
3type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c"
type T2 = Exclude<string | number | (() => void), Function>; // string | number
Extract 提取
从T中提取U,类似于找到唯一值
原理1
type Extract<T, U> = T extends U ? T : never;
例子1
2type T0 = Extract<"a" | "b" | "c", "a" | "f">; // "a"
type T1 = Extract<string | number | (() => void), Function>; // () =>void
Omit 排他
Omit类似于排他思想 除了自己之外的都能用
原理1
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
例子1
2
3
4
5
6
7
8
9
10
11
12interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Omit<Todo, "description">;
const todo: TodoPreview = {
title: "Clean room",
completed: false,
};
NonNullable 排null&undefined
NonNullable用于过滤null和undefined
原理1
type NonNullable<T> = T extendsnull | undefined ? never : T;
例子1
2type T0 = NonNullable<string | number | undefined>; // string | number
type T1 = NonNullable<string[] | null | undefined>; // string[]
Parameters
用于获取参数类型组成的元组类型
原理1
2type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any
? P : never;
例子1
2
3
4type A = Parameters<() =>void>; // []
type B = Parameters<typeofArray.isArray>; // [any]
type C = Parameters<typeofparseInt>; // [string, (number | undefined)?]
type D = Parameters<typeofMath.max>; // number[]
tsconfig.json
他是ts的重要配置文件,相当于vue.config.js
,在这里讲几个重要的配置
files -设置要编译的文件名字
include -设置需要进行编译的文件,支持路径模式匹配
exclude -设置无需编译的文件,支持路径模式匹配
compilerOptions -设置与编译流程相关的选项
compilerOptions
1 | { |