likes
comments
collection
share

Vue3 + TypeScript 实践总结

作者站长头像
站长
· 阅读数 14

为什么 TypeScript

Vue2.x 对 TypeScript 的支持是硬伤,而 TypeScript 对于大型项目更加友好,团队协作时强类型总比约定更严谨。所以,一些大型工程在技术选型时,抛弃 Vue 而选择 React。尤其是在 React16.8+ 函数式组件有状态以后。

为什么会出现 setup

首先,Vue 只是 MVC 中的 View 层,需要将 Model、Controller 分离开才能最大化地降低耦合、增加复用。即Vue 单文件组件应该只是处理视图层的业务逻辑及渲染即可,而 Controller 的业务逻辑不应该在单文件组件中,因此诞生了 Vuex,而 Vuex 更多是一个全局状态管理,在处理一些业务逻辑、数据交互、组件通信上,使用 Vuex 来管理显得太重了。因而在 View 与 Controller 的业务逻辑处理上,有了一个真空地带,导致我们在 methods 中耦合了大量的业务逻辑,随着版本不断迭代,一旦体量变大,公共方法的抽离、代码维护的成本指数倍增长,组件已变得不可复用,难以阅读,能做的只能继续往上面堆;

其次,一些需要响应式的公共属性、方法,我们通常引入 mixin 来隔离,但是 mixin 不太好管理,容易污染。

不难发现,业务逻辑耦合度变高后,最终导致 Vue 单文件组件的作为 View 层,页面 UI 哪怕相似度极高,也可能无法复用。

高内聚、低耦合,对于 Vue 来说,即需要能够分离业务逻辑关注点、易管理、响应式的API,setup 为了这而产生。

Vue 实践

Props 自身的 type 属性无法描述更复杂的数据结构,TS 会报错

Vue 提供了类型别名 PropType,用于定义接收的更复杂的 props 属性结构。

import { PropType, defineComponent, RendererElement } from 'vue';

interface ITabItem {
    text: string,
    id: string | number,
    active?: boolean
}
    
const XComponent = defineComponent({
    props: {
        tabList: {
            type: Array as PropType<Array<ITabItem>>,
            default: () => []
        }
    },
    setup() {
        return (): RendererElement => (
        	<div>{
                props.tabList.map(tab => {
                	return <p key={tab.id}>{tab.text}</p>
            	})
            }</div>
        )
    }
})

单文件 vue 组件与 tsx 函数组件间的 slot 使用

单文件 vue 组件互相之间的 slot 使用,可阅读官方文档 单文件插槽

jsx/tsx 组件互相之间的 slot 使用,可以阅读文档 jsx/tsx 插槽

单文件同 jsx、tsx 组件之间的 slot 使用,示例如下:

// vue 单文件中引入 tsx 函数式组件 XInput
<section>
    <x-input v-model="inputInfo.value">
        <template v-slot:search>
            <p>This is search slot.</p>
        </templatet>
    </x-input>
</section>

// XInput
const SearchBox = (props: IChildProp) => {
    const value = props.value;
    const clickEv = () => {
        // todo
        props.clickHandler?.();
    }
    return (
        <div class="search-box" onClick={clickEv}>
            <img src={searchIcon} />
        </div>
    )
}
export default defineComponent({
    setup(props, { emit, slots }) {
        const value = ref(props.value);
        const inputEv = (ev: Event) => {
            value.value = (ev.target as HTMLInputElement).value;
            emit('update:input', value);
        }
        const searchEv = ():void => {
            // todo
        }
        return (): RendererElement => (
            <div class="x-input">
            	<input class="input-el" value={value.value} onInput={inputEv} />
                {
                    slots.search &&
                    (<>
                        <SearchBox clickHandler={searchEv} value={value.value} />
                        {slot.search?.()} // 渲染成 <p>This is a search slot.</p>
                    </>)
                }
            </div>
        )
    }
})

自定义hooks——将“动作”转成 “状态”

// todo

巧用类型收缩解决 TypeScript 报错

1、类型断言

类型断言可以明确告诉 TypeScript 值的详细类型,在某些场景,我们非常确认它的类型,即使与 typescript 推断出来的类型不一致。

2、类型守卫

  • typeof: 用于判断 number, string, boolean 或 symbol 四种类型
function padLeft(value: string | number) {
    if (typeof value === string) {
        console.log(value.length);
    }
}
  • instanceof: 用于判断一个实例是否属于某个类

    class Man {
        handsome = 'handsome'
    }
    class Woman {
        beautiful = 'beautiful'
    }
    function Human(arg: Man | Woman) {
        if (arg instanceof Man) {
            console.log(arg.handsome);
        } else {
            console.log(arg.beautiful);
        }
    }
    
  • in: 用于判断一个属性/方法是否属于某个对象

    interface B {
        b: string
    }
    interface A {
        a: string
    }
    function foo(x: A | B) {
        if ('a' in x) {
            return x.a;
        }
        return x.b;
    }
    
  • 字面量类型保护

    有些场景,使用 in, instanceof, typeof 太过麻烦。这时候可以自己构造一个字面量类型

    type Man = {
        handsome: 'handsome';
        type: 'man';
    }
    type Woman = {
        beautiful: 'beautiful';
        type: 'woman';
    }
    function Human(arg: Man | Woman) {
        if (arg.type === 'man') {
            console.log(arg.handsome);
        } else {
            console.log(arg.beautiful);
        }
    }
    

3、双重断言

有些时候使用 as 也会报错,因为 as 断言的时候也不是毫无条件的。它只有当 S 类型是 T 类型的子集时,S能被断言成T。

所以面对这样的情况,只想暴力解决问题,可以使用双重断言,首先断言成兼容所有类型的 unknown。

function handler(event: Event) {
    const element = event as HTMLElement; // Error
    // 'Event' 和 'HTMLElement' 中的任何一个都不能复制给另一个
}

// 首先断言成 unknown
function handler(event: Event) {
    const element = (event as unknown) as HTMLElement;
}

4、用户自定义的类型保护

假若我们一旦检查过类型,就能在之后的每个分支里清楚地知道它的类型就好了。 TypeScript 里的类型保护机制使其成为现实。

类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。要定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个 类型谓词

function isFish(pet: Fish | Bird): pet is Fish {
    return (<Fish>pet).swim !== undefined;
}

在这个例子里,pet is Fish 就是类型谓词。谓词为 parameterName is Type 这种形式,parameterName 必须是来自当前函数签名里的一个参数名。

当使用一些变量调用 isFish 时,TypeScript 会将变量缩减为那个具体的类型,只要这个类型与变量的原始类型是兼容的。

// TypeScript 不仅知道在 if 分支里 pet 是 Fish 类型;还清楚在 else 分支里,一定不是 Fish 类型,一定是 Bird 类型。
if (isFish(pet)) {
    pet.swim();
} else {
    pet.fly();
}

5、使用 never 收窄类型

interface Foo {
    type: 'foo'
}

interface Bar {
    type: 'bar'
}
// 一个联合类型  All
type All = Foo | Bar;

function handleValue(val: All) {
    switch (val.type) {
        case 'foo':
            break;
        case 'bar':
            break;
        default:
            // val 在这里是 never
            const exhaustiveCheck: never = val;
            break;
    }
}

注意在 default 里面我们把收窄为 never 的 val 赋值给了一个现实声明为 never 的变量。如果一切逻辑正常,那么这里应该能够编译通过。但是假如后来有一天你的同事改了 All 的类型:

type All = Foo | Bar | Baz

然而他忘记了在 handleValue 里面加上针对 Baz 的处理逻辑,这个时候 default 里面 val 会被收窄为 Baz,导致无法复制给 never,产生一个编译错误。所以通过这个办法,可以确保 handleValue 总是穷尽了所有的 All 的可能类型。

6、非空断言运算符!

这个运算符可以用在变量名或者函数名之后,用来强调对应的元素是 非 null|undefined 的,应用场景,特别适合我们已经明确知道不会返回空值的场景,从而减少冗余的代码判断。

this.$refs.container!.scrollWidth

7、键值获取 keyof

keyof 可以获取一个类型所有键值,返回一个联合类型

type Person = {
    name: string;
    age: number;
}
type PersonKey = keyof Person; // 'name' | 'age'

keyof 的一个典型应用场景是限制访问对象的 key 合法化,因为 any 做索引是不被接收的

function getValue (p: Person, k: keyof Person) {
	return p[k];
}

巧用高级类型灵活处理数据

1、类型索引

使用索引类型,编译器就能够检查使用了动态属性名的代码。

function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
    return names.map(n => o[n]);
}

interface Person {
    name: string;
    age: number;
}
let person: Person {
    name: 'Jack',
    age: 18
}

let strings: string[] = pluck(person, ['name']);

编译器会检查 name 是否真的是 Person 的一个属性。

keyof T 索引类型查询操作符。对于任何类型 T,keyof T 的结果为 T 上已知的公共属性名的联合。

let personProps: keyof Person; // 'name' | 'age';

T[K] 索引访问操作符。在这里,类型语法反映了表达式语法。就像索引类型查询符一样,可以在普通的上下文中使用 T[K],只要确保类型变量 K extends keyof T 就可以了。

  • keyof: 获取类型上的 key 值

  • extends: 泛型里面的约束

  • T[P]: 获取对象 T 相应 K 的元素类型

    type Partial<T> = {
        [P in keyof T]?: T[P]
    }
    // [自定义变量名 in 枚举类型]: 类型
    

在使用 props 的时候,有时候全部属性都是可选的,如果一个一个属性写 ?,大量的重复动作,这种时候可以直接使用 Partial

// 如何用 ts 声明 AnimalMap?
const AnimalMap = {
    cat: { name: '猫', title: 'cat' },
    dog: { name: '狗', title: 'dog' },
    frog: { name: '蛙', title: 'wa' }
}

// Record 第一个泛型传入对象的 key 值,第二个传入对象的属性
type Record<K extends string, T> = {
    [P in K]: T;
}

type AnimalType = 'cat' | 'dog' | 'frog';
interface AnimalDescription {
    name: string;
    title: string
}

const AnimalMap: Record<AnimalType, AnimalDescription> {
    cat: { name: '猫', title: 'cat' },
    dog: { name: '狗', title: 'dog' },
    frog: { name: '蛙', title: 'wa' }
}

2、可辨识联合

合并单例类型,联合类型,类型保护和类型别名来创建一个叫做 可辨识联合 的高级模式,它也称作 标签联合代数数据 数据类型。可辨识联合在函数式编程中很有用处。

三要素:

  • 具有普通的单利类型属性 —— 可辨识的特征
  • 一个类型别名包含了那些类型的联合 —— 联合
  • 此属性上的类型保护
interface Square {
    kind: 'square';
    size: number
}
interface Rectangle {
    kind: 'rectangle';
    width: number;
    height: number;
}
interface Circle {
    kind: 'circle';
    radius: number;
}

type Shape = Square | Rectangle | Circle;

function assertNever(x: never): never {
    throw new Error("Unexpected object: " + x);
}

// 可辨识联合
function area(s: Shape) {
    switch (s.kind) {
        case 'square':
            return s.size * s.size;
        case 'rectangle':
            return s.height * s.width;
        case 'circle':
            return Math.PI * s.radius ** 2;
        default:
            return assertNever(s); // 处理遗漏的 case
    }
}

assertNever 检查 s 是否为 never 类型 —— 即为出去所有可能情况后剩下的类型。

装饰器(Decorator)的理解

装饰器(Decorator) 是一种特殊类型的声明,它能够被附加到类声明方法访问符属性参数 上。装饰器使用 @expression 这种形式,expression 求值后必须为一个函数,它会在运行时被调用,被装饰声明信息作为参数传入,它添加额外的方法或属性到基类上。为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。

我们简单的理解装饰器,可以认为它是一种包装,对对象、方法、属性的包装。当我们需要访问一个对象的时候,如果我们通过这个对象外围的包装去访问的话,被这个包装附加的行为就会被触发。例如一把加了消声器的枪,消声器就是一个装饰器,但是它和原来的枪成为一个整体,开枪的时候消声器就会发生作用。

装饰器组合

多个装饰器可以同时应用到一个声明上:

  • 书写在同一行上:

    @f @g x
    
  • 书写在多行上(常用):

    @f
    @g
    x
    

当多个装饰器应用于一个声明上,它们求值方式与 复合函数 相似。

当多个装饰器应用在一个声明上时会进行如下步骤的操作(调用栈,后进先出):

1、由上至下依次对装饰器表达式求值

2、求值的结果会被当作函数,由下至上依次调用

leDecorator是welcome函数的装饰器:

function leDecorator(target, propertyKey: string, descriptor: PropertyDescriptor): any {
    console.log('enter decorator');
	const oldValue = descriptor.value;
    descriptor.value = function() {
        console.log(`Calling "${propertyKey} with",` arguments, target, target instanceof Object ,descriptor);
        const value = oldValue.apply(null, [arguments[1], arguments[0]]);
        console.log('Function is executed');
        return `${value}; This is awesome`;
    };
    return descriptor;
}

class JSMeetup {
    public speaker = 'Ruban';
    // @leDecorator
    welcome(arg1, arg2) {
        console.log(`Arguments Received are ${arg1} ${arg2}`);
        return `${arg1} ${arg2}`;
    }
}

const meetup = new JSMeetup();
console.log(meetup.welcome('World', 'Hello'));

// 注释掉 @leDecorator
/*
	Arguments Received are World Hello
	World Hello
*/ 

// 放开注释
/*
	enter decorator
	Calling "welcome" with [Arguments] { '0': 'World', '1': 'Hello' } {}, true, {
        value: [Function (anonymous)],
        writable: true,
        enumerable: false,
        configuarble: true
    }
    Arguments Received are Hello World
    Function is executed
    Hello World; This is awesome
*/ 

TypeScript 中共计有 5 类装饰器:

  • 方法装饰器
  • 属性装饰器
  • 类装饰器
  • 参数装饰器
  • 访问器装饰器

方法装饰器

方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。它会被应用到方法的属性描述符上,可以用来监视、修改或者替换方法定义。方法装饰器不能用在声明文件(*.d.ts),重载或者任何外部上下文(比如 declare 的类)中。

下面是方法装饰器的定义:

MethodDecorator = <T>(target: object, key: string, decriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;

参数:

  • target: 对于静态成员来说是类的构造函数(construcor),对于实例成员是类的原型对象(Object)
  • key: 成员的名字(被装饰的函数名)
  • descriptor: 成员(被装饰的函数)的属性描述符

下面是一个方法装饰器(@writable)的例子,应用于 Greeter 类的方法上:

function writable(value: boolean) {
    return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.writable = value;
        return descriptor;
    }
}

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    // @writable(false)
    greet() {
        return `Hello, ${this.greeting}`;
    }
}

const greeter = new Greeter('Tony');
greeter.greet = () => { return 'Hello, Jack' };
console.log(greeter.greet());

// 注释修饰器writable
// Hello, Jack

// 放开注释
// Hello, Tony

这里的 @writable(false) 是一个 装饰器工厂 。当装饰器 @writable(false) 被调用时,它会修改属性描述符的 writable 属性。

注意点:

  • 装饰器在 class 被声明的时候执行,而不是class实例化的时候
  • 方法装饰器返回一个值
  • 存储原有的描述符并且返回一个新的描述符是推荐的做法。这在多描述符应用场景下非常有用
  • 设置描述符value的时候,不要使用箭头函数

属性装饰器

通过属性装饰器,可以重新定义 getter, setter, 修改 enumerable, writable, configurable 等属性,也可以用来记录这个属性的元数据。

属性装饰器定义如下:

PropertyDecorator = (target: object, key: string) => void;

属性装饰器表达式会在运行时当作函数调用,传入下列2个参数

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象,即属性拥有者
  • 成员的名字,属性名

用 Object.defineProperty 来实现一个简单的属性装饰器

function realName(target, key: string): any {
    let _val = target[key];
    
    const getter = function() {
        return `${_val} Ma`;
    }
    const setter = function(newVal) {
        _val = newVal;
    }
    
    Object.defineProperty(target, key, {
        get: getter,
        set: setter
    })
}

class JSMeetup {
    // @realName
    public myName = 'Tony';
    greet() {
        return `Hi, I'm ${this.myName}`;
    }
}

const meetup = new JSMeetup();
console.log(meetup.greet());

// 注释装饰器realName
// Hi, I'm Tony

// 放开注释
// Hi, I'm Tony Ma

metadata(元数据)

描述数据的数据,也叫元数据。它是对一直心系的一种集合称谓,它可以是描述某项具体的数值,也可以是描述影像或声音的内容,也可能 只是一些注释。它们往往跟随着对象文件,让我们得以更全面的了解对象相关的信息。eg. 照片的元数据包括图像的尺寸、拍摄时间、光圈、快门、GPS等;视频文件画面尺寸、视频和音频的编码、时长等等。

import 'reflect-metadata';

const formatMetadataKey = Symbol('format');

function format(formatString: string) {
    return Reflect.metadata(formatMetadataKey, formatString);
}

function getFormat(target: any, propertyKey: string) {
    return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}

class Greeter {
    @format('Hello, %s')
    greeting: string;
    
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        let formatString = getFormat(this, 'greeting');
        return formatString.replace('%s', this.greeting);
    }
}

const greeter = new Greeter('Tony');
console.log(greeter.greet());
// Hello, Tony

这个 @format('Hello, $s') 装饰器是一个 装饰器工厂。被调用时,添加一条这个属性的元数据。当 getFormat 被调用时,它读取对应属性的元数据。

类装饰器

类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。

类装饰器定义如下:

ClassDecorator = <T extends Function>(target: T) => T

类装饰器表达式会在运行时当作函数调用,类的构造函数作为其唯一的参数。

无返回值:

// 通过 Object.seal 密封此类的构造函数和原型
function sealed(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}

// @sealed
class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
        this.constructor.prototype.toString = function() {};
        console.log(this.constructor.prototype);
    }
    greet() {
        return `Hello, ${this.greeting}`;
    }
}

const greeter = new Greeter('Tony');
console.log(greeter.greet());

// 注释装饰器 sealed
// { toString: [Function (anonymous)] }
// Hello, Tony

// 取消注释
// TypeError: Cannot add property toString, object is not extensible

有返回值:

interface Extra {
    extra: string
}

function AwesomeMeetup<T extends { new (...args: any[]): {} }>(constructor: T): T {
    return class extends constructor implements Extra {
        extra = 'Tadah!';
        speaker: string = 'Ragularuban';
        hello: string = 'Hello';
    }
}

// @AwesomeMeetup
class JSMeetup {
    public speaker = 'Ruban';
    public hello: string;
    constructor(message: string) {
        this.hello = message;
    }
    greet() {
        return `Hi, I'm ${this.speaker}`;
    }
}

const meetup = new JSMeetup('Hi') as JSMeetup & Extra;
console.log(meetup.greet());
console.log(meetup.extra);

class A extends JSMeetup {}

const a = new A('HeHe');
console.log(a.greet());

// 注释装饰器 AwesomeMeetup
/*
	Hi, I'm Ruban
	undefined
	HeHe, I'm Ruban
*/

// 取消注释
/*
	Hello, I'm Ragularuban
	Tadah!
	Hello, I'm Ragularuban
*/

参数装饰器

参数装饰器应用于类构造函数或方法声明,往往用来对特殊的参数进行标记,然后在方法装饰器中读取对应的标记,执行进一步的操作。

参数装饰器往往搭配方法装饰器一起使用

参数装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  • 成员的名字
  • 参数在函数参数列表中的索引

参数装饰器只能用来监视一个方法的参数是否被传入,参数装饰器的返回值会被忽略。

function logParameter(target: any, key: string, index: number) {
    const metadataKey = 'myMetaData';
    if (Array.isArray(target[metadataKey])) {
        target[metadataKey].push(index);
    } else {
        target[metadataKey] = [index];
    }
    console.log('param target: ', target);
}

function logMethod(target, key: string, descriptor: any): any {
    const originalMethod = descriptor.value;
    // 参数装饰器、方法装饰器的第一个参数 target 是同一个引用
	console.log('method target: ', target);
    descriptor.value = function(...args: any[]) {
        const metadataKey = 'myMetaData';
        const indices = target[metadataKey];
        for (let i = 0; i < args.length; i++) {
            if (indices.indexOf(i) !== -1) {
                args[i] = 'Abrakadabra';
            }
        }
        const result = originalMethod.apply(this, args);
        return result;
    }
    return descriptor;
}

class JSMeetup {
    // @logMethod
    public saySomething(something: string, @logParameter somethingElse: string): string {
        return `${something} : ${somethingElse}`;
    }
}

const meetup = new JSMeetup();
console.log(meetup.saySomething('Something', 'Something Else'));

// 注释掉方法装饰器 logMethod
/*
	param target: { myMetaData: [1] }
	Something : Something Else
*/

// 取消注释
/*
	param target: { myMetaData: [1] }
	method target: { myMetaData: [1] }
	Something : Abrakadabra
*/

访问器装饰器

访问器装饰器应用于访问器的属性描述符,可以用来监视、修改或替换一个访问器的定义。

不能向多个同名的 get/set 访问器应用装饰器,get/set 只能选择其一应用

访问器装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  • 成员的名字
  • 成员的属性描述符
function configurable(value: boolean) {
    return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        // descriptor.writable = false 会报错后再赋值就会报错
        descriptor.configurable = value;
    }
}

class Point {
    private _x: number;
    private _y: number;
    constructor(x: number, y: number) {
        this._x = x;
        this._y = y;
    }
    
    @configurable(false)
    get x() {
        return this._x;
    }
    
    @configurable(false)
    get y() {
        return this._y;
    }
}

function reWrite(value: number) {
    return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.get = function() {
            return value;
        }
        return descriptor;
    }
}

class Greeter {
	_x: number;
    _y: number;
    constructor(x: number, y: number) {
        this._x = x;
        this._y = y;
    }
    
    set x(value: number) {
        this._x = value;
    }
    
    // @reWrite(999)
    get x() {
        return this._x;
    }
    
    getPoint() {
        console.log(`x: ${this.x}, y: ${this._y}`);
    }
}

const greeter = new Greeter(100, 100);
greeter.getPoint();

// 注释掉访问器装饰器 reWrite
// x: 100, y: 100

// 去取消注释
// x: 999, y: 100

TypeScript 解决了哪些痛点

属性、方法等访问/调用错误,空指针等基于非预期类型的错误

常见错误:

  • Uncaught TypeError: Cannot read property
  • TypeError: 'undefined' is not an object
  • TypeError: null is not an object
  • TypeError: 'undefined' is not a function
  • TypeError: Cannot read property 'length'
  • ReferenceError: event is not defined

enum 枚举

常见的应用场景是描述某同一特征的常量、元数据,例如任务类型可能被定义为 '1', '2', '3',不同的任务类型有不同的处理逻辑,通过 switch/if 判断时,我们直接 if(taskType === '1') 是不可以的,枚举(enum) 的出现解决了这个问题,并且让代码的可阅读性更高。

enum TaskType {
    DOWNLOAD: '1',
    SHARE: '2',
    FEED: '3'
}
switch (type) {
    case DOWNLOAD:
        // todo
        break;
    // ...
}

enum ResStatus {
    EXPECT: '1001',
    ACT_OVER: '3001',
    NO_LOGIN: '3010'
}

装饰器

一些底层的 Class 不能过多的业务逻辑或者耦合,或者在某些场景中,需要对元数据进行操作(元编程),或在真实应用中,不能尽善尽美,装饰器可以看做是一种润滑剂、补丁、对元数据的操作(元编程),或者说可以称之为面向切面编程(AOP)。

泛型

TypeScript 高级用法(字节前端)

泛型在 TS 中承载了从静态定义到动态调用的桥梁,同时也是 TS 对自己类型定义的元编程。

基本使用

泛型可以用在普通类型定义,类定义、函数定义上:

// 普通类型定义
type Dog<T> = { name: string, type: T }
// 普通类型使用
// 需要把泛型类型也写上去
const dog: Dog<number> = { name: 'wang', type: 3 }

// 类定义
class Cat<T> {
    private type: T;
    constructor(type: T) {
        this.type = type;
    }
}
// 类使用
// 变量能够推断出来,可以省略泛型书写
const cat: Cat<number> = new Cat<number>(20); // const cat = new Cat(20);

// 函数定义
function swipe<T, U>(value: [T, U]): [U, T] {
    return [value[1], value[0]];
}
// 函数使用
// 变量能够推断出来,可以省略泛型书写
swipe<Cat<number>, Dog<number>>([cat, dog]); // swipe([cat, dog]);

泛型约束

有的时候,可以不用关注泛型具体的类型

function fill<T>(length: number, value: T): T[] {
    return new Array(length).fill(value);
}

有时候,需要限定类型,可以使用关键字 extends

function sum<T extends number>(value: T[]): number {
    let count = 0;
    value.forEach(v => count += v);
    return count;
}

这样可以 sum([1, 2, 3]) 的方式调用,而 sum(['1', '2', '3']) 则无法通过编译。

泛型推断 infer

一般搭配泛型条件语句使用,所谓推断,就是不用预先指定在泛型列表中,在运行时会自动判断,不过先得预先定义好整体的结构。

type Foo<T> = T extends {t: infer Test} ? Test: string;

首先看 extends 后面的类容,{t: infer Test} 可以看成一个包含 t 属性的类型定义,这个 t 属性的 value 类型通过 infer 进行推断后会赋值给 Test 类型,如果泛型实际参数符合 {t: infer Test} 的定义,那么返回 Test 类型,否则默认返回 string 类型。

// 返回 string,因为 number 不是一个包含 t 属性的对象类型
type One = Foo<number>;

// 返回 boolean,因为泛型参数中有 t 属性的对象类型,使用了 infer 对应的类型,即 boolean
type Two = Foo<{t: boolean}>

// 同理,返回一个函数类型,() => void
type Three = Foo<{a: numbem, t: () => void}>

Tips

  • Vue 提供 interface RendererElement,可用于 渲染函数/tsx 中作为返回值校验

  • 联合类型中巧用 never 类型,可以处理一些遗漏的 case

  • interface HTMLInputElement 的 value 属性获取输入框的值 (event.target as HTMLInputElement).value

  • 数据结构过于复杂、嵌套太深,类型判断无法推断时,可以试着使用 ?. ?.() obj as IObject 等方式解决

  • usehooks 中如果涉及到元素获取,最好使用 vue 单文件组件,ref 可以指向唯一值,tsx 等渲染函数中无法通过 ref 定位元素,当组件复用高时,通过 document.querySelector 拿到的不一定是预期元素(可以通过唯一 id 或 props 来解决)

  • setup 方法不能 async

  • 数组是一串相同类型的数据的集合,内存中连续存放,实际上存放的是地址,指向对象实例,JS 中数组并不是真正意义上的数组,故 TypeScript 中的定义数组时,需要指定数组元素中的类型

    // js 的数组可以是任意类型
    const hunmanList = [18, 'Tony', { like: 'Reading' }]
    
    interface Human {
        age: number,
    	name: string
    }
    
    const humanList: Human[]
    const humanList: Array<Human>
    
  • defineAsyncComponent 用于引用子组件处,接收的参数是一个 Promise<Component>

    const BannerDownload = defineAsyncComponent((): Promise<Component> => import('@/components/common/BannerDownload.vue'));
    
  • 通 react 一样,类型断言最好使用 el as HTMLElement 而非尖括号 <HTMLElement>el

  • Vue3 暴露的 interface, type 在文件 @vue/runtime-core/dist/runtime-core.d.ts

  • 当提示不能对 null 进行类型断言时,先对变量类型签名 unknown,使用 unknown 替代 any,既灵活又可以保证类型安全

    • Vue3 setup 中获取模板引用 ref,需要 return,示例如下:
    onMounted(() => {
        const videoBox = ref(null);
        onMounted(() => {
            const el: unknown = videoBox.value;
            console.log(window.getComputedStyle(el as HTMLElement).width);
        })
        return { videoBox }
    })
    
  • TypeScript 内置类型在 node_modules/typescript/lib 下的 *.d.ts 文件中

  • 全局安装 ts-node 然后在 .ts 文件中第一行 *#!/usr/bin/env ts-node* 终端中输入路径即刻直接执行 ts 文件

  • 构造函数的类型可以通过 { new (...args: any[]): {} } 来描述