typescript基础笔记

2022-03-28 18:49:24 浏览数 (1)

typescript早在2013年就发布了第一个正式版本,印象中一直到了19年才大火起来。三年过去了,一直是可用可不用的状态,于是很多人都没学习使用。直到react和vue开始捆版上了ts,前端圈也开始了“内卷”,ts已经是不得不用的状态了。

这次分享的是自己学习过程觉得掌握了就可以上手的内容,上手了之后通过项目多实践, 实践过程再学习深入的内容,应该就能比较快的掌握。

学习过程贴的代码都是在在线的这个平台的演练场调试的:https://www.typescriptlang.org/zh/

tips:
  1. ts最终都会编译成js,添加的类型最终都会被删除,只是为了开发的时候提示
  2. ts一切类型校验的目标都是为了安全
  3. ts冒号(:)后面的都是类型
  4. 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还有很多需要学习,这些只是简单的了解,然后在开发过程中再慢慢学习其它内容。

0 人点赞