typescript早在2013年就发布了第一个正式版本,印象中一直到了19年才大火起来。三年过去了,一直是可用可不用的状态,于是很多人都没学习使用。直到react和vue开始捆版上了ts,前端圈也开始了“内卷”,ts已经是不得不用的状态了。
这次分享的是自己学习过程觉得掌握了就可以上手的内容,上手了之后通过项目多实践, 实践过程再学习深入的内容,应该就能比较快的掌握。
学习过程贴的代码都是在在线的这个平台的演练场调试的:https://www.typescriptlang.org/zh/
tips:
- ts最终都会编译成js,添加的类型最终都会被删除,只是为了开发的时候提示
- ts一切类型校验的目标都是为了安全
- ts冒号(:)后面的都是类型
- ts中小写的(string)叫类型,描述基础类型,大写的(String)是类,描述的是实例
一、基础类型
boolean、number、string
代码语言:javascript复制let num: number = 3;
let have: boolean = true;
let str: string = 'str';
null、undefined
null和undefined对应的类型就是本身。不开启严格模式的时候可以赋值给任何类型, 也就是任何类型的子类型。一般会开启,所以null和undefined只能是自己的类型。
strictNullChecks:严格检测null undefined
代码语言:javascript复制let nu: null = null;
let un: undefined = undefined;
any
ts中所有的类型都可以是any类型,使用any类型也意味着放弃类型校验。除非特殊情况,并不建议使用。
unknown
为了解决any带来的问题所出现的类型,也是所有类型都可以是unknown类型。unknown类型被当作安全的类型。
unknown只能赋值给unknown或者any:
代码语言:javascript复制let a: unknown = 1;
let b: any = a;
let c: number = a;//err
unknown类型不能进行运算、调用属性、当作函数:
代码语言:javascript复制let a: unknown;
a.toString();//err
a();//err
let b = a 1;//err
unknow使用的时候要把类型具体化,缩小使用范围:
代码语言:javascript复制function isString(val: unknown): void{
if(typeof val === 'string'){
val.toString();
val.toFixed();//err
}
}
unknown会被当作安全类型的原因是不能进行运算、调用属性、当作函数, 使用的时候类型要具体化,缩小使用范围,这样就可以避免any的那些不安全的副作用。
void
void类型只是当作函数的没有返回值的时候使用,表示没有任何类型。函数不加void类型, 默认返回void,所以如果函数没有返回值的时候void加不加感觉都可以。
Array
ts的数组有两种写法,常规写法:
代码语言:javascript复制let arr1: string[] = [''];
let arr2: (string | number)[] = ['', 3];
另外一种泛型写法,会比较少用,把[]变成Array,类型写到尖括号里面:
代码语言:javascript复制let arr1: Array<string> = [''];
let arr2: Array<string | number> = [''];
一般开放会用到数组对象([{}]),声明一般两种方法:
代码语言:javascript复制interface ListItem{
name: string
id: number
label: string
}
let GroupList1:ListItem[] = [{
name: '',
id: 1,
label: ''
}]
let GroupList2:Array<ListItem> = [{
name: '',
id: 2,
label: ''
}]
Tuple
元组,ts衍生的一种类型,限制数组的长度和顺序:
代码语言:javascript复制let tuple:[string, number, boolean] = ['str', 3, true];
元组已知数组的长度和每个子元素的类型,不能多也不能少。可以push已知类型的子元素,但是无法使用。
Enum
ts的枚举只是为了清晰地表达意图或创建一组有区别的用例。
数字枚举:
代码语言:javascript复制enum Direction {
Up = 1,
Down,
Left,
Right
}
console.log(Direction.Up);//0
console.log(Direction[0]);//Up
数字枚举会自动增长,如果第一个不初始化赋值会从0开始。枚举可以通过名字和下标访问,枚举值和枚举名字互相映射。编译后的代码:
代码语言:javascript复制"use strict";
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 0] = "Up";
Direction[Direction["Down"] = 1] = "Down";
Direction[Direction["Left"] = 2] = "Left";
Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));
字符串枚举:
代码语言:javascript复制enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
字符串枚举获取值的方式不能使用下标。
异构枚举: 数字和字符串混合使用,几乎不会使用,因为没什么意义:
代码语言:javascript复制enum Direction {
Up = "Up",
Down = "DOWN",
Left = 1,
Right,
}
object
object表示的是非原始数据类型, 也就是除number,string,boolean,symbol,null或undefined之外的类型。
代码语言:javascript复制let a: object = [3, 'str'];
let b: object = {obj: 3};
let c: object = function(){}
never
never意味着永远达不到,函数返回值使用,报错、死循环可以做到, 通常用来校验代码完整性,实现类型的绝对安全,一般很少做这么严格的校验。
二、断言
ts的断言其实就是手动断定是什么类型,主要是为了欺骗编辑器,只在编译阶段起作用, 编译之后断言就移除了,所以使用的时候一定要注意自己断言的结果。
非空断言!
断定某个变量不是空的,也就是不会是undefined或者null:
代码语言:javascript复制let a: number;
a =1;//err
let b: number;
b! =1;
function setName(name: string | undefined){
let myName1: string = name;//err
let myName2: string = name!;
}
as语法
把某个类型断定成某个类型:
代码语言:javascript复制function getVal(obj: string | number){
(obj as number).toFixed();
}
上面的obj可能是string可能是number,string没有toFixed,这时候用as语法, 表示确定obj一定是number。
三、类型保护(类型守卫)
类型保护是运行时的一种检测,当一个变量不确定是什么类型的时候,可以调用共有的属性和方法, 特有的属性和方法就需要配合类型保护。
js提供typeof 、instanceof、 in
代码语言:javascript复制//typeof
function getVal(obj: string | number){
if(typeof obj === 'number'){
obj.toFixed();
}
}
//in
type Obj1 = {
a: number
}
type Obj2 = {
b: number
}
function getTh(obj: Obj1 | Obj2){
if('a' in obj){
console.log(obj.a)
}
}
//instanceof
type Fn = () => {}
function run(fn: Fn | number){
if(fn instanceof Function){
fn();
}
}
自定义
除了js提供的,ts也可以自定义类型保护,使用is关键字, is关键字一般用于函数返回值类型中,判断参数是否属于某一类型,并根据结果返回对应的布尔类型:
代码语言:javascript复制function isObject(val: any): val is Object{
return Object.prototype.toString.call(val) === '[Object object]'
}
type Dog = {
type: 'dog'
eat: () => {}
}
type Cat = {
type: 'cat'
speak: () => {}
}
//如果没有animal is Dog,下面调用animal.eat()是会报错,ts中,直接返回true或者false并不能判断是Dog还是Cat
function getIs(animal: Dog | Cat): animal is Dog{
return animal.type === 'dog';
}
function isDog(animal: Dog | Cat){
if(getIs(animal)){
animal.eat();
}
}
四、联合类型和交叉类型
联合类型其实就是用|组合,交叉类型用&:
代码语言:javascript复制//联合类型
let a: string | number;
type A = string | number;
type B = string | boolean;
//交叉类型
let b: A & B = 'str';
交叉类型如果是基本数据类型,合并的时候同名会出现never:
代码语言:javascript复制type Obj1 = {
a: string
b: number
}
type Obj2 = {
a: number
c: string
}
type Obj = Obj1 & Obj2;
Obj相当于:
代码语言:javascript复制type Obj = {
a: never;
b: number;
c: string;
}
出现never其实就是报错,一般不会出现。
五、接口interface和类型别名type
接口和类型别名在ts中非常重要,这两个在一般情况可以通用,也有区别。一定要记住,声明interface和type的时候用类型,实现的时候用具体的值。
接口interface
接口用来描述数据的形状,没有具体的实现(抽象的),接口可以描述函数、对象、类。(语法上分号和逗号可写可不写)
描述对象:
代码语言:javascript复制interface Person{
age: number
name: string
}
可选、只读属性:
代码语言:javascript复制interface Person{
readonly age: number
name?: string
}
一般接口定义的属性一定要实现,修饰符?表示可选,函数参数也是这样使用。只读是实现的时候初始话可以赋值,之后赋值就会报错。
任意属性:
代码语言:javascript复制interface Person{
age: number
name: string
[other: string]: any
}
语法就是key用[]包裹,key用什么类型,值一般用any,否则已经定义的和这边的都要统一一个类型。
扩展属性:
接口定义好之后,想要扩展属性,一般使用继承或者自动合并:
代码语言:javascript复制interface Person{
age: number
name: string
}
//自动合并
interface Person{
sex: string
}
//继承
interface Son extends Person{
sex: string
}
其它方法用as断言、使用ts的兼容性几乎不用。
类型别名type
类型别名也可以用来描述对象和函数,还可以描述原始类型、元组、联合类型等。语法上面类型别名是=。
代码语言:javascript复制type Person = {name: string, age: number};
type Name = string;
type Arr = [string, number];
type Uni = Person | Arr;
接口和类型别名的区别
接口和类型别名很多时候会分不清,因为用起来一个样,把区别理清楚了就知道两者的通用和不同。
函数语法区别较大:
代码语言:javascript复制type Fn1 = (a: string) => number
interface Fn2{
(a:string): number
}
let fn: Fn2 = (a: string) => {
return 5
}
接口还是对象形式,参数:返回,类型别名则直接是箭头函数。
type可以使用联合类型和具体的值和元组:
代码语言:javascript复制interface Obj{
a: string
}
type Tu = [string];
type A = 1;
type Uni1 = Tu | A | Obj;
type Uni2 = Tu & A & Obj;
type的联合类型可以联合接口,使用联合类型就相当于扩展type,没办法扩展自身。
接口可以继承和多继承,接口还可以继承类型别名,会自动合并,可扩展:
代码语言:javascript复制type Obj1 = {a: string}
interface Obj2{
b: string
}
interface Obj2{
c: string
}
interface Obj3 extends Obj1, Obj2{
d: string
}
type可以内部使用in语法,interface无法使用in语法:
代码语言:javascript复制type Obj1 = {a: string, b: number}
type Obj2<T> = {[K in keyof T]: T[K]}
这个在后面泛型使用的时候很有用。
类可以实现接口或类型别名,但类不能实现联合类型的别名:
代码语言:javascript复制interface Obj1{
a: string
}
type Obj2 = {b: string};
type Obj3 = Obj1 | Obj2;
class O1 implements Obj1{
a = 'a'
}
class O2 implements Obj2{
b = 'b'
}
//err
class O3 implements Obj3{
b = 'b'
}
六、泛型
泛型在ts中非常重要,使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。这样用户就可以以自己的数据类型来使用组件。在定义类型的时候还不能确定是什么类型,在使用的时候才能确定类型。根据传入的类型决定类型。
语法:
代码语言:javascript复制function fn<T, K>(a: T, b: K): K{
return b
}
尖括号里面相当于参数,可以是任意的名字,一般使用:
- T:type
- K:key
- V:value
- E:element 泛型是类型,并不是具体的参数。
调用的时候如果不具体类型,会根据参数推论出:
代码语言:javascript复制fn(1, '');
//不常用
fn<number, string>(1, '');
接口、类型别名泛型:
代码语言:javascript复制interface Eat<T>{
(a: T): T
}
let eat: Eat<string> = () => 'a';
eat('');
interface Food<T>{
name: T
}
let per: Eat<Food<string>> = (a) => a;
per({name: 'w'})
type Speak<T> = (a: T) => T;
let speak: Speak<string> = (a) => a;
speak('');
泛型约束extends
用来约束泛型的范围,约束要满足约束的特点,满足是拥有特性,只要含有要求的属性
代码语言:javascript复制interface Ex{
name: string
}
function fn<T extends Ex>(obj: T){
}
fn({name: '', age: 3})
这边是只要有name属性就可以,如果我返回值设置T,是不行的:
代码语言:javascript复制interface Ex{
name: string
}
function fn<T extends Ex>(obj: T): T{
return T //err
}
fn({name: '', age: 3})
T只是约束了具有name属性,但是不能保证T一定是原来的T,可以增删属性。
typeof、keyof、in
typeof:可以用来获取一个变量声明或对象的类型
代码语言:javascript复制interface Person{
name: string
age: number
}
let person: Person = {name: '', age: 3}
//等价
type PersonS = typeof person;
type PersonO = Person;
keyof:获取某种类型的所有键,其返回类型是联合类型
代码语言:javascript复制interface Obj{
name: string
age: number
}
type Obj1 = keyof Obj;//"name" | "age"
let obj1: Obj1 = 'name';
let obj2: Obj1 = 'age';
in:用来遍历,看结果是遍历联合类型
代码语言:javascript复制type keys = 'name' | 'age';
type Obj1 = {
[K in keys]: any
};
let obj1: Obj1 = {name: '', age: 3};
条件类型分发
泛型中如果通过条件判断返回不同的类型,放入的是联合类型(|),具备分发功能:
代码语言:javascript复制type Obj1 = {name: string};
type Obj2 = {age: number};
type JudgeObj1<T extends Obj1 | Obj2> = T extends Obj1 ? Obj1 : Obj2;
type IsObj1 = JudgeObj1<Obj1>//{name: string}
分发的理解就是,T会先跟Obj1判断,再和Obj2判断,而不是Obj1 | Obj2看成一个整体。
通过内置类型Exclude可以更好的理解(删除第二个参数存在的类型):
代码语言:javascript复制//type Exclude<T, U> = T extends U ? never : T
type Ex = Exclude<number | string | boolean, number>
- number extends string ? never : number得到number
- string extends string ? never : number得到never
- boolean extends string ? never : number得到boolean
- 所以最终结果是string | boolean。会分别把没一个类型去校验。
内置类型、infer
内置类型和infer可以后面好好了解,这边列举几个常用的内置类型
- Readonly:只读
- Exclude:删除第二个参数存在的类型
- Extract:返回符合第二个参数的类型
- Required:全部变成必填
- Partial:让所有属性都变成可选
- NonNullable:去除null和undefined
- Pick:在对象中挑选
- Omit:忽略对象中的
.d.ts
ts会检测根目录下所有.d.ts文件,里面用declare声明的都是全局的。declare声明的都没有具体的实现,.d.ts只是为了使代码不报错,没有任何实际功能。
比如用script引入jq,直接$()会报错,在.d.ts声明:
代码语言:javascript复制declare function $(){}
ts还有很多需要学习,这些只是简单的了解,然后在开发过程中再慢慢学习其它内容。