9

总结知识点

vue3需要用ts可能是小伙伴没开始玩转vue3的最大障碍, 经过1年vue3+ts开发实战, 我总结了必须要会的ts知识点(内容只有官网内容的1/4), 方便大家学习vue3的时候查阅, 保证大家学会了本文轻松玩转vue3开发.

🌕祝大家中秋快乐

vue3基础

如果你还没有开始接触vue3, 也可以看我整理的vue3基础教程(带视频).

基础数据类型

课程中的代码大家可以在这里进行测试, 可以试试看到ts编译后的js是什么样的.
image.png
https://www.typescriptlang.org/zh/play

类型标注

typescript中我们可以给变量标记类型, 后续的代码中ts会自动校验变量是否有错误的读/写操作.

let is:boolean;
is =  true;
is =  123; // 报错, 提示number类型不能赋值给boolean类型的变量

语法很简单, 就是在变量后面加上":"和类型, 这个动作叫类型标注.

类型自动推断

如果变量的值是一个字面量, ts可以自动推断出类型.

let is = false;
is = true;
is =  123; // 报错, 提示number类型不能赋值给boolean类型的变量

let o = {a:1,b:'2'}
o.a ='33' // 报错, 不能吧string分配给number

字面量

字面量就是数据的表现形式,我们看到就知道这个数据的内容, 比如上面"="右侧的数据. 本身概念很简单, 单独提出只是怕第一次听到这个词的同学会有疑惑.

其他情况

还有几种情况ts会自动推断变量类型, 下节课我们展开讲.

基础数据类型

接下来我们看看系统中都有哪些数据类型.

boolean

布尔类型.

let is:boolean;
is =  true;
is =  123; // 报错, 提示is是数字不能赋值给boolean类型的变量

number

数字类型, 不仅仅支持10进制.

let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
let binaryLiteral: number = 0b1010;
let octalLiteral: number = 0o744;
decLiteral = '123'; // 报错

string

字符串类型.

let s1:string = 'hello world!'; 
let s2:string = 'hello ${name}`;
s1 = 123 // 报错

数组

数组类型有2种表示方式.

"类型+[]"
let numbers:number[] = [1,2,3,4,5];

// number|string代表联合类型, 下面的高级类型中会讲
let numbers:(number|string)[] = [1,2,3,4,'5'];
Array<类型>
let numbers:Array<number> = [1,2,3,4,5];

元组(Tuple)

元组类型表示一个已知元素数量类型数组, 各元素的类型不必相同:

let list1:[number, string] = [1, '2', 3]; // 错误, 数量不对, 元组中只声明有2个元素
let list2:[number, string] = [1, 2]; // 错误, 第二个元素类型不对, 应该是字符串'2'
let list3:[number, string] = ['1', 2]; // 错误, 2个元素的类型颠倒了
let list4:[number, string] = [1, '2']; // 正确

枚举(enum)

枚举是ts中有而js中没有的类型, 编译后会被转化成对象, 默认元素的值从0开始, 如下面的Color.Red的值为0, 以此类推Color.Green为1, Color.Blue为2:

enum Color {Red, Green, Blue}
// 等价
enum Color {Red=0, Green=1, Blue=2}

我们还可以反向通过值得到键:

enum Color {Red=1, Green=2, Blue=4}
Color[2] === 'Green' // true

看下编译成js后的枚举代码, 你就明白为什么可以反向得到键值:

var Color;
(function (Color) {
    Color[Color["Red"] = 0] = "Red";
    Color[Color["Green"] = 1] = "Green";
    Color[Color["Blue"] = 2] = "Blue";
})(Color || (Color = {}));
// Color的值为: {0: "Red", 1: "Green", 2: "Blue", Red: 0, Green: 1, Blue: 2}

any(任意类型)

any代表任意类型, 也就是说, 如果你不清楚变量是什么类型, 就可以用any进行标记, 比如引入一些比较老的js库, 没有声明类型, 使用的时候就可以标记为any类型, 这样ts就不会提示错误了. 当然不能所有的地方都用any, 那样ts就没有使用的意义了.

void

void的意义和any相反, 表示不是任何类型, 一般出现在函数中, 用来标记函数没有返回值:

function abc(n:number):void{
    console.log(n);
}

void类型对应2个值, 一个是undefined,一个null:

const n1:void = undefined;
const n2:void = null;

null 和 undefined

默认情况下null和undefined是所有类型的子类型, 比如:

const n1:null = 123;
const n2:undefined = '123';

never

never表示不可达, 用在throw的情况下:

function error():never{
    throw '错了!';
}

unknown

unknown表示未知类型, 他和any一样的地方是标记后的变量可以随意赋值, 但是在访问变量上的属性的时候要先断言变量的类型才可以操作.

let a:unknown;
a = '汉字';
a.length // 提示类型为unknown, 不能访问length
类型断言(as或<>)

当操作的时候我都可以强制告诉ts该变量现在是什么类型. 比如在上面的a变量是unknown在ts中我们是没法对他进行操作的, 这时候就需要进行"类型断言":

let a:unknown;
a = '汉字';

(a as string).length

// 等价写法
(<string>a).length

2种语法效果一样, 个人建议大家使用第一种, 书写更容易.

symbol

let s:symbol;
s = Symbol('a');
s = 1 //报错 

object

object表示非原始类型, 也就是除number/ string/ boolean/ symbol/ null/ undefined之外的类型:

let o1:object = [];
let o2:object = {a:1,b:2};

但是, 我们实际上基本不用object类型的, 因为他标注的类型不具体, 一般都用接口来标注更具体的对象类型, 下节课我们讲"如何定义接口".

自动类型推断(类型保护)

上节我们说ts可以从"字面量"推断出变量类型, ts的这种"自动推断"的能力官方叫做"类型保护".

let o = {a:1,b:'2'} // 可以识别对象中的字段和类型
o.a ='33' // 报错, 不能吧string分配给number

本节我们继续介绍其他会触发自动推断的情况.

typeof

判断类型.

let n:number|string = 0.5 < Math.random()? 1:'1';

// 如果没有typeof, n*=2会报错, 提示没法推断出当前是number类型, 不能进行乘法运算
if('number' === typeof n) {
    n*= 2;
} else  {
    n= '2';
}

类型中的typeof

之前说的"typeof"是js中的api, 实际在ts中也有"typeof"关键字, 可以提取js对象的类型.

function c(n:number){
  return n;
}

// typeof c获取函数c的类型
function d(callback :typeof c){
  const n = callback(1);
  console.log(n);
}

在vscode的提示中我们可以看到ts已经正确的推导出了"typeof c"的类型.
image.png

instanceof

判断是否实例.

let obj = 0.5 < Math.random() ? new String(1) : new Array(1);

if(obj instanceof String){
    // obj推断为String类型
    obj+= '123'
} else {
    // obj为any[]类型
    obj.push(123);
}

in

判断是否存在字段.

interface A {
  x: number;
}

interface B {
  y: string;
}

function ab(q: A | B) {
  if ('x' in q) {
    // q: A
  } else {
    // q: B
  }
}

自定义类型保护(is)

前面我们都是用系统自带的关键词触发"类型保护", 下面我们自定义一个:

  1. 首先我们需要定义一个函数, 用来判断并返回boolean值.
  2. 返回值不要直接标注boolean, 而是使用"is", is前面一般是参数, 后面是判断后的类型.
  3. 比如下面的"isBird"函数, 如果返回值是true, 那么"animal"就是Bird.
  4. 配合"if"使用"isBird"时, 会触发自动类型推断.

    interface Animal {
     name: string;
    }
    
    interface Bird {
     name: string;
     hasWing: boolean;
    }
    
    // 关键看函数返回值的类型标注
    function isBird(animal:Animal): animal is Bird {
     return animal.name.includes('雀');
    }
    
    
    // 使用isBird对触发自动类型推断
    const who = { name: '金丝雀' };
    if (isBird(who)) {
     who.hasWing = true;
    }

"is"在vue3源码中应用

了解即可.

// 是否是对象
export const isObject = (val: any): val is Record<any, any> =>
  val !== null && typeof val === 'object'

// 是否ref对象
export function isRef(v: any): v is Ref {
  return v ? v[refSymbol] === true : false
}

// 是否vnode
export function isVNode(value: any): value is VNode {
  return value ? value._isVNode === true : false
}

// 是否插槽节点
export const isSlotOutlet = (
  node: RootNode | TemplateChildNode
): node is SlotOutletNode =>
  node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.SLOT

接口

一种定义复杂类型的格式, 比如定义对象类型:

interface Article {
    title: string;
    count: number;
    content:string;
    fromSite: string;
}

const article: Article = {
    title: '为vue3学点typescript(2), 类型',
    count: 9999,
    content: 'xxx...',
    fromSite: 'baidu.com'
}

// 报错不存在titleA字段, 同时缺少title/count/content/fromSite
const article: Article = {
    titleA: '为vue3学点typescript(2), 类型',
}

当我们给article赋值的时候, 如果任何一个字段没有被赋值或者字段对应的数据类型不对, ts都会提示错误, 避免字段名拼写错误或漏写.

扩展性

有时对象的字段可能是允许未知的, 比如Article可能还有"a"字段"b"字段, 为了兼容这种情况, 我们改造下:

interface Article {
    title: string;
    count: number;
    content:string;
    fromSite: string;
      [k:string]:string;
}

通过"[k:string]:string"我们允许Article类型中出现任意字符串键值, 只要保证内容也为字符串即可.

非必填(?)

用"?"标记fromSite字段为非必填.

interface Article {
    title: string;
    count: number;
    content:string;
    fromSite?: string; // 非必填
}

// 缺少fromSite字段也不会报错
const article: Article = {
    title: '为vue3学点typescript(2), 类型',
    count: 9999,
    content: 'xxx...',
}

只读(readonly)

用"readonly"标记字段不可修改.

interface Article {
    readonly title: string;
    count: number;
    content:string;
    fromSite?: string; // 非必填
}

let a:Article =  {title:'标题',count:1,content:'内容',}
a.title = 123; // 报错, title是只读属性

定义函数

// 声明接口
interface Core {
    (n:number, s:string):[number,string]
}

// 使用接口进行标注
const core:Core = (a,b)=>{
    return [a,b];
}

函数也是对象, 所以接口Core还可以声明属性.

interface Core {
    (n:number, s:string):[number,string];
        n:number;
}

const core:Core = (a,b)=>{
    return [a,b];
}
core.n = 100;

定义类

约束类用"implements"关键字.

// 定义
interface Animal {
    head:number;
    body:number;
    foot:number;
    eat(food:string):void;
    say(word:string):string;
}

// implements
class Dog implements Animal{
    head=1;
    body=1;
    foot=1;

    constructor(head:number){
        this.head = head;
    }
    eat(food:string){
        console.log(food);
    }
    say(word:string){
        return word;
    }
}

对比下可以发现: interface部分指定以了类的实例属性, 而构造函数并没有定义, 定义一下构造函数:

继承

让一个接口拥有另一个接口的所有特性.

interface Animal {
    color: string;
}

interface Cat extends Animal {
    foot: number;
}

let cat:Cat;
cat.color = "blue";
cat.foot = 10;

合并

相同名字的接口会自动合并.

interface Apple {
    color: string;
}
interface Apple {
    weight: number;
}

等价于

interface Apple {
    color: string;
    weight: number;
}

类型别名(type)

通过"type"关键字可以给已有的类型换个名字.

type N = number;
type S = string;

interface F1{
    (a:number):string;
}
type F2 = F1;

乍一看, 你一定觉得这用处不大, 而且定义函数"interface"也是可以实现的, 那么接下来讲下他的特别之处.

定义函数

前面说过接口可以定义函数, type也可以, 但是语法有一点不一样:

interface F1{
    (a:number):string;
}

type F2 = (a:number)=>string;

"F1"和"F2"定义的是相同的类型, 主要参数和返回值中间的他俩的区别, 一个是":"一个是"=>".

定义字面量类型

这是"type"能做到而"interface"不能实现的.

type N = 1|2|3;
type S = 'a'|'b'|'c';

// 正确
const n1:N = 1;
// 错误, 类型N中没有11;
const n2:N = 11

联合类型( | )

把多个类型用"|"连接到一起, 表示类型之间"或"的关系.

type F1 = (a:string|number)=>any;
const f1:F1 = (a)=>a;

// 正确
f1(1);
f1('A');

// 错误, 参数类型应该是string或number
f1({a:1});
interface Cat  {
      hand:string;
    foot: string;
}

interface Duck{
    body:string;
}

type Animal  = Cat|Duck;

// 错误, Cat没有body字段
const animal:Cat = {hand:'手',body:'身体',foot:'脚'};
// 正确
const animal:Animal = {hand:'手',body:'身体',foot:'脚'};

交叉类型( & )

把多个类型用"&"连接到一起, 取类型的并集.

interface A {a:number};
interface B {b:string};
type AB = A & B;

const a:A = {a:1};
const b:B = {b:'1'};
// 错误, 缺少a字段
const ab1:AB = {b:'2'};
// 正确
const ab2:AB = {a:1,b:'2'};

泛型

泛型可以理解为使用"不确定的类型"去描述类/函数/接口, 而在使用的时候再确定具体的类型. 这个"不确定的类型"我们叫他类型变量, 可以表示任意类型, 一般我们用大写字母表示类型, 比如"T"或"U".

function echo<T>(input:T):T{
    return input;
}

泛型函数

在上面我们定义了一个"泛型函数", 首先定义了类型变量T, 用"<>"包围, 然后标注函数的参数和返回值的类型都是"T".
虽然使用"echo"的时候参数"input"可以是任意类型, 但是不同于标注"any"类型, 这里函数的返回值会根据参数类型的变化而同步变化.

// n1是number类型
const n1 = echo<number>(1);

// s会推断为string类型
const s = echo<string>('1') 

如果参数是字面量, 那么使用函数的时候可以省略前面的"<number>"和"<string>", ts可以自动推断出参数的类型, 从而推断出类型变量T的值:

// n1是number类型
const n1 = echo(1);

// s会推断为string类型
const s = echo('1') 

泛型类

在类名后面通过"<>"声明一个类型变量U, 类的方法和属性都可以用这个U类型,

class Person<U> {
    who: U;
    
    constructor(who: U) {
        this.who = who;
    }

    say(code:U): string {
        return this.who + ' :i am ' + code;
    }
}

接下来我们使用下泛型类:

let a =  new Person<string>('詹姆斯邦德');
a.say(007) // 错误, 会提示参数应该是个string
a.say('007') // 正确

我们指定类型变量为(string),告诉ts这个类的U是string类型, 通过Person的定义, 我们知道say方法的参数也是string类型, 所以a.say(007)会报错, 因为007是number. 所以我们可以通过传入型变量来约束泛型.

泛型方法

和泛型函数的定义方式一样:

class ABC{
    // 输入T[], 返回T
    getFirst<T>(data:T[]):T{
        return data[0];
    }
}

泛型类型

我们可以用类型变量去描述一个类型, 这里可以结合type和interface实现:

type

type A<T> = T[];
// 正确
const a: A<number> = [1,2,3]
// 错误
const b: A<number> = ['1','2','3'];

泛型接口(interface)

interface Goods<T>{
    id:number;
    title: string;
    size: T;
}

// 正确
let apple:Goods<string> = {id:1,title: '苹果', size: 'large'};
let shoes:Goods<number> = {id:1,title: '苹果', size: 43};

默认值

type A<T=string> = T[];
// 正确
const a  = ['1','2','3'];

interface Goods<T=string>{
    id:number;
    title: string;
    size: T;
}
// 正确
let apple:Goods<string> = {id:1,title: '苹果', size: 'large'};

泛型约束

function echo<T>(input: T): T {
    console.log(input.name); // 报错, T上不确定是否由name属性
    return input;
}

前面说过T可以代表任意类型, 但并不是所有类型上都有"name"字段, 通过"extends"可以约束"T"的范围:

// 现在T是个有name属性的类型
function echo<T extends {name:string}>(input: T): T {
    console.log(input.name); // 正确
    return input;
}

多个类型变量

可以同时使用多个类型变量.

function test<T,U>(a:T,b:U):[T,U]{
    return [a,b];
}

不滥用泛型

泛型主要是为了约束, 或者说缩小类型范围, 如果不能约束功能, 就代表不需要用泛型:

function convert<T>(input:T[]):number{
    return input.length;
}

这样用泛型就没有什么意义了, 和any类型没有什么区别.

工具类型

系统预设了很多工具类型.

Partial<T>

让属性都变成可选的

type A  = {a:number, b:string}
type A1 = Partial<A> // { a?: number; b?: string;}

Required<T>

让属性都变成必选.

type A  = {a?:number, b?:string}
type A1 = Required<A> // { a: number; b: string;}

Pick<T,K>

只保留自己选择的属性, K代表要保留的属性键值

type A  = Pick<{a:number,b:string,c:boolean}, 'a'|'b'>
type A1 = Pick<A, 'a'|'b'> //  {a:number,b:string}

Omit<T,K>

实现排除已选的属性.

type A  = {a:number, b:string}
type A1 = Omit<A, 'a'> // {b:string}

Record<K,T>

创建一个类型,K代表键值的类型, T代表值的类型

type A1 = Record<string, string> // 等价{[k:string]:string}

Exclude<T,U>

过滤T中和U相同(或兼容)的类型

type A  = {a:number, b:string}
type A1 = Exclude<number|string, string|number[]> // number

// 兼容
type A2 = Exclude<number|string, any|number[]> // never , 因为any兼容number, 所以number被过滤掉

Extract<T,U>

提取T中和U相同(或兼容)的类型.

type A  = {a:number, b:string}
type A1 = Extract<number|string, string|number[]> // string

NonNullable

剔除T中的undefined和null.

type A1 = NonNullable<number|string|null|undefined> // number|string

ReturnType

获取T的返回值的类型.

type A1= ReturnType<()=>number> // number

InstanceType

返回T的实例类型

ts中类有2种类型, 静态部分的类型和实例的类型, 所以T如果是构造函数类型, 那么InstanceType可以返回他的实例类型:

interface A{
    a:HTMLElement;
}

interface AConstructor{
    new():A;
}

function create (AClass:AConstructor):InstanceType<AConstructor>{
    return new AClass();
}

Parameters

获取函数参数类型, 返回类型为元祖, 元素顺序同参数顺序.

interface A{
    (a:number, b:string):string[];
}

type A1 = Parameters<A> // [number, string]

ConstructorParameters

获取构造函数的参数类型, 和Parameters类似, 只是T这里是构造函数类型.

interface AConstructor{
    new(a:number):string[];
}

type A1 = ConstructorParameters<AConstructor> // [number]

更多

其实你可以在"node_modules/typescript/lib/lib.es5.d.ts"中找到上面所有类型的定义, 随着ts的更新, 这里可能会有更多的类型.

image.png

自定义工具类型

上节课我们学了很多系统预设的工具类型, 接下来我们学习自己实现工具类型.

keyof

先看系统预设的Partial的源码.

type Partial<T> = {
    [P in keyof T]?: T[P];
};

这里出线了2个新的关键词"keyof"和"in", "keyof"的作用是取对象的所有键值, 比如:

type A = keyof {a:string,b:number} // "a"|"b"

in

用来遍历联合类型.

type A = "a"|"b"
type B = {
    [k in A]: string;
}
// B类型:
{
    a: string;
    b: string;
}

extends和三目运算

之前我们在"泛型约束"章节使用了"extends"用来约束类型范围, 这里我们结合三目运算实现类型的条件判断.
语法为:

type A = T extends U ? X : Y;

如果U的范围大于T, 比如U有的字段T中都有, 那么A的值为X, 反之为Y.

type T1 = {a:number,x:number};
type U1 = {a:number,b:number};
type A1 = T1 extends U1 ? number : string; // string

type T2 = {a:number,b:number};
type U2 = {a:number};
type A2 = T2 extends U2 ? number : string; // number

系统预设类型中的"Exclude"也是如此实现:

type Exclude<T, U> = T extends U ? never : T;

这里表示如果过滤掉T中有U中也有的类型:

type T1 = number|string;
type U1 = number|string[];
type A = Exclude<T1,U1> // string

infer(类型推断)

单词本身的意思是"推断", 这里表示在extends条件语句中声明待推断的类型变量. 先看下预设类型Parameters:

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

上面声明一个P用来表示...args可能的类型, 如果(...args: infer P)范围大于T, 那么返回...args对应的类型, 也就是函数的参数类型, 反之返回never.
注意: 开始的T extends (...args: any) => any用来输入的T必须是函数.

应用infer

接下来我们利用infer来实现"删除元祖类型中第一个元素":

export type Tail<T extends unknown[]> = T extends [a: any, ...args: infer P] ? P : never

type A = Tail<[number,string,null]> // [string,null]

类型断言(as)

有些情况下系统没办法自动推断出正确的类型, 就需要我们自行标记下:

document.body.addEventListener('click', e=>{
    e.target.innerHTML = 'test'; // 报错, 因为e.target的类型是EventTarget, 其上没有innerHTML
    (e.target as HTMLElement).innerHTML = 'test';// 正确
});

因为"addEventListener"方法不仅仅dom元素支持, 所以系统的类型描述中"e.target"不能直接标注成"HTMLElement".

但是上面代码在使用的时候, 我们就知道e.target是dom元素, 所以就需要我们告诉ts:"这是dom", 这就是"类型断言", 语法为"as", 还可以用"<>"表示:

document.body.addEventListener('click', e=>{
    (e.target as HTMLElement).innerHTML = 'test';
      // 等价
      (<HTMLElement>e.target).innerHTML = 'test';
});

在axios中的应用

axios是开发最常用的http请求库, 我们异步请求来的数据, 他的类型一定是未知的, 这就需要我们标注类型:

axios.get<{n:number}>('/xx').then(data=>{
    console.log(data.data.n+1);
});

注意"get"方法后面我们传入了"类型参数", 说明get方法在定义的时候使用了"泛型". 在"<>"中传入的类型, 表示的就是返回数据的类型, 这里相当于标注返回值类型.

但是假如你使用了"axios的拦截器"改变了返回值, 那么在这里标注就不好使了, 比如:


http.interceptors.response.use(function (response) {
   // 让返回数据值返回data字段的值
   return data.data;
}, function (error) {
    return Promise.reject(error);
});

由于我们修改返回值, 但是axios并不知道他自己的返回值变了, 所以还像上面那么标注返回值类型肯定不行了, 这时我们就可以使用类型断言:

axios.get('/xx').then(data=>{
    console.log((data as {n:number}).n+1);
});

枚举 (Enum)

枚举是typescript自己的数据类型, javascript中并没有这个类型, 但是这并没有兼容问题, 因为在编译后枚举会被编译成js代码.

使用"enum"关键字定义枚举 , 可以把enum和Class的格式很像, 成员用"="赋值:

enum Direction {Up, Right, Down, Left};

看下编译后的js代码:

var Direction;
(function (Direction) {
    Direction[Direction["Up"] = 0] = "Up";
    Direction[Direction["Right"] = 1] = "Right";
    Direction[Direction["Down"] = 2] = "Down";
    Direction[Direction["Left"] = 3] = "Left";
})(Direction || (Direction = {}));

从这里我们可以到作为枚举成员的值和键都被当做对象的键值进行了双向赋值, 实际我们访问的就是这个对象, 所以枚举支持双向访问:

Direction[0] // 'Up'
Direction.Up // 0

应用场景

主要用来做常量的定义,枚举名相当于一个作用域, 可读性更好些, 同时ts对他的类型推断也更清晰.

值递增

枚举的成员的值如不指定, 那么默认从0递增赋值:

Direction.Up // 0
Direction.Right // 1
Direction.Down // 2
Direction.Left // 3

如果某个成员被赋值了数字, 那么从该数字递增:

enum Direction {Up=5, Right, Down=10, Left};
Direction.Up // 5
Direction.Right // 6
Direction.Down // 10
Direction.Left // 11

值只可以是number和string

// 正确
enum Direction {Up = '上', Right = 1};

// 错误
enum Direction {Up = ['上]', Right = {r:1}};

也可以是计算值

如果表达式的结果是number或string也是可以作为枚举成员的值的:

function greenValue(){
    return 2;
}

enum Color {Red = 1, Green = greenValue(), Blue='aaa'.length};

但是要注意: 如果某一个成员没有值, 那么他前面的成员必须是字面量的值才可以:

function greenValue(){
    return 2;
}
// 错误, Blue要求Green是个字面量的数字或字符串
enum Color {Red = 1, Green = greenValue(), Blue};

// 没问题, 首个成员前面没有成员, 所以不受上面的限制
enum Color1 {Red, Green = greenValue(), Blue='aaa'.length}; 

常量枚举(const)

使用"const enum"来声明常量枚举,

const enum Enum {
  A = 1,
  B = A * 2,
}

Enum.A
Enum.B

不同于普通的枚举, 常量枚举在编译成js后, 不会生成多余的代码:

// 编译后的结果
"use strict";
1 /* A */;
2 /* B */;

开发中我个人更喜欢使用常量枚举, 因为他不会生成多余的代码, 同时还能保证良好的语义.

什么是声明文件

如果ts项目中引入了js文件, 那么就需要写"声明文件"来告诉ts引入js文件中的变量类型.

应用场景

  1. 如果在html中引入了第三方js库, 比如"jq", 那么在ts代码中要使用"$"变量就需要"**声明**", 因为ts不知道$的存在.
  2. 对全局对象"window/doucument"等上缺少(或自定义)的字段进行补充.
  3. 已存在的npm中的js包, 补充其类型声明.
  4. 对npm上已经存在的ts包的类型进行扩充, 比如向vue实例上增加的$xx属性在ts中都需要自己声明才能使用.

    举个例子

    针对第一种情况实现代码如下:

    // global.d.ts
    declare var $: (selector: string) => any;

    这里"declare"是声明的关键字, 其他场景实例在后面的课中讲解.

    npm上的声明文件

    像jq这种常见的js库, 其实已经有人写好了声明文件, 并发布到了npm中, 比如jq的声明文件的包是"@types/jquery", lodash的声明文件是"@types/lodash", 如果你通过npm安装的包没有声明文件你可以执行对应的安装命令:

    npm i @types/xx

    文件位置

    声明文件一般放在项目根目录下, 这样整个项目的ts文件都可以读取到声明的类型, 命名格式为"xx.d.ts".
    image.png

    全局interface / type

    上面我们说是给变量增加类型声明, 实际我们还可以在"global.d.ts"中添加接口和type, 添加后接口就会变成全局的, 这样在项目的任意文件中都可以读取到该类型.

    // global.d.ts
    interface Abc{
     n:number
    }
    // index.ts
    const abc:Abc = {n:100};

    引入(///)

    如果你的想要复用已存在的类型, 我们可以通过"///"语法去引入:

    // test.d.ts
    type ABC = number;
    // global.d.ts
    /// <reference path="./test.d.ts" />
    declare const a:ABC // number

    现在我们对声明文件有了初步的认识, 下一节开始我们学习自己编写声明文件.

全局变量的声明

对于全局变量的声明我们使用"declare"关键字实现. 再次强调下声明文件我们一般放在项目的根目录, 为了语义起名叫"global.d.ts".

下面展示各种类型数据的声明写法.

declare var

声明全局变量

// global.d.ts
declare var n:number;
declare let s:string;
declare const version:string;

这里的"var"也可以是"let"或"const", 语义和js中的一致, 根据情况可自行决定.

declare function

声明全局函数

// global.d.ts
declare function translate(words: string): string;

declare enum

声明全局枚举

// global.d.ts
declare enum Directions {
    Up,
    Down,
    Left,
    Right
}

declare class

声明全局类

// global.d.ts
declare class Animal {
    name: string;
    constructor(name: string);
    sayHi(): string;
}

declare namespace

声明命名空间, 可以理解为声明一个对象.

// global.d.ts
declare namespace req {
    function get(url: string, params?: Record<string,any>): Promise<any>;
}

扩展全局变量

如果对全局变量进行了扩展, 那么就需要声明扩展后的字段.比如给"String"增加cut方法.

String.prototype.cut = function(n){
    return this.substr(0,n);
};

'12345'.cut(3); // 123

对应的声明文件:

// global.d.ts
interface String {
  // 自定义字段
  cut(s:string): string;
}

因为"String"类型是ts系统自带的声明文件中写好的, 本身就是用interface描述的, 还记得interface类型会自动合并的特性吧, 这里直接写一个同名interface实现类型合并.
image.png
[
](https://github.com/microsoft/...)

系统的声明文件

js的api在ts中都是有类型声明的, 我们可以在项目的"node_modules/typescript/lib/"中看到所有的系统声明文件.
image.png
也可以在github上查看

带浏览器前缀的API

我观察发现系统自带的声明, 不会对带"浏览器前缀"的API进行声明, 如果我们自己写插件就需要补充这部分, 比如"requestFullScreen"这个api:

// global.d.ts
interface HTMLElement {
    webkitRequestFullscreen(options?: FullscreenOptions): Promise<void>;
    webkitRequestFullScreen(options?: FullscreenOptions): Promise<void>;
    msRequestFullscreen(options?: FullscreenOptions): Promise<void>;
    mozRequestFullScreen(options?: FullscreenOptions): Promise<void>;
}

源码: https://github.com/any86/be-full/blob/master/global.d.ts

扩展npm模块的类型(declare module)

npm下载的"包"自带了声明文件, 如果我们需要对其类型声明进行扩展就可以使用"declare module"语法.

让vue3支持this.$axios

// main.ts
app.config.globalProperties.$axios = axios;

功能上我们实现了"this.$axios", 但是ts并不能自动推断出我们添加了$axios字段, 也就是在组件内使用this.$axios会提示缺少类型, 所以添加如下声明文件:

// global.d.ts

import { ComponentCustomProperties } from 'vue'

// axios的实例类型
import { AxiosInstance } from 'axios'

// 声明要扩充@vue/runtime-core包的声明.
// 这里扩充"ComponentCustomProperties"接口, 因为他是vue3中实例的属性的类型.
declare module '@vue/runtime-core' {
  
  // 给`this.$http`提供类型
  interface ComponentCustomProperties {
    $axios: AxiosInstance;
  }
}

这里扩充"ComponentCustomProperties"接口, 因为他是vue3中实例的属性的类型.

更全面的例子

上面的例子中我们扩充了原声明中的interface, 但是如果导出是一个Class我们该如何写呢? 下面我们对"any-touch"的类型进行扩充, 这里"any-touch"的默认导出是一个Class. 假设我们对"any-touch"的代码做了如下修改:

  1. 导出增加"aaa"变量, 是string类型.
  2. 类的实例增加"bbb"属性, 是number类型.
  3. 类增加静态属性"ccc", 是个函数.

    // global.d.ts
    
    // AnyTouch一定要导入, 因为只有导入才是扩充, 不导入就会变成覆盖.
    import AnyTouch from 'any-touch'
    
    declare module 'any-touch' {
     // 导出增加"aaa"变量, 是个字符串.
     export const aaa: string;
         
     export default class {
       // 类增加静态属性"ccc", 是个函数.
       static ccc:()=>void
       // 类的实例增加"bbb"属性, 是number类型.
       bbb: number
     }
    }

    注意: AnyTouch一定要导入, 因为只有导入才是类型扩充, 不导入就会变成覆盖.

测试下, 类型都已经正确的添加:

// index.ts
import AT,{aaa} from 'any-touch';

const s = aaa.substr(0,1);

const at = new AT();
at.bbb = 123;

AT.ccc = ()=>{};

对非ts/js文件模块进行类型扩充

ts只支持模块的导入导出, 但是有些时候你可能需要引入css/html等文件, 这时候就需要用通配符让ts把他们当做模块, 下面是对".vue"文件的导入支持(来自vue官方):

// global.d.ts
declare module '*.vue' {
  import { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}
// App.vue
// 可以识别vue文件
import X1 from './X1.vue';
export default defineComponent({
    components:{X1}
})

声明把vue文件当做模块, 同时标注模块的默认导出是"component"类型. 这样在vue的components字段中注册模块才可以正确识别类型.

vuex

下面是vuex官方提供的, 在vue的实例上声明增加$store属性, 有了前面的知识, 看这个应该很轻松.

// vuex.d.ts

import { ComponentCustomProperties } from 'vue'
import { Store } from 'vuex'

// 声明要扩充@vue/runtime-core包的声明
declare module '@vue/runtime-core' {
  // declare your own store states
  interface State {
    count: number
  }

  // provide typings for `this.$store`
  interface ComponentCustomProperties {
    $store: Store<State>
  }
}

并非全部内容

到此声明的内容就都结束了, 但实际上还有些"模块声明"的方式并没有覆盖到, 因为本课程的最终目的是基于vue3开发暂不涉猎npm包的开发,所以其他的内容就不展开了, 有需要的同学可以看ts文档来学习, 有了本文的基础, 相信你会很轻松学会更多.

微信群

感谢大家的阅读, 如有疑问可以加我微信, 我拉你进入微信群(由于腾讯对微信群的100人限制, 超过100人后必须由群成员拉入)

github

我个人的开源都是基于ts的, 欢迎大家访问https://github.com/any86

image.png


铁皮饭盒
5k 声望1.2k 粉丝

喜欢写程序: [链接]