最新公告
  • 欢迎您光临网站无忧模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 跟Vue3源码学习TypeScript

    正文概述 掘金(闹闹前端)   2021-01-15   967

    说在开始

    TypeScript一直是方兴未艾的前端编程工具语言,它极大的提高了代码的可读性、可维护性。 同时在静态编译能提早暴露出很多问题,比如因不小心写错变量名、传递给函数的参数类型或个数不对、大对象上属性名的获取,很大的提高了开发体验、效率以及测试效率。

    目前很多库也都改用TypeScript。

    比如Vue2是用flow语言开发,Vue3便采用TypeScript。

    ReactRouter V6版本也使用了tsx语法,还有它内部应用的 history V5版本也采用了TypeScript。

    本篇我们结合Vue3源码,从实践中学习TypeScript的用法。

    类型断言

    在文件packages/shared/src/index.ts中第22行至第26行

    export const babelParserDefaultPlugins = [
      'bigInt',
      'optionalChaining',
      'nullishCoalescingOperator'
    ] as const
    

    上面代码中的as是类型断言之一,也是我最喜欢用的

    它的作用就是断定babelParserDefaultPlugins是const类型

    第二种类型断言即尖括号的类型断言

    let str = <string>bar;
    

    它的作用是断定bar的类型是string。

    这两种的类型断言作用是一样的,区别只是写法上不同。

    但是需要注意的是第二种写法,在结合JSX的语法后容易带来解析上的困难。

    因此在.tsx文件里会禁止使用尖括号的类型断言

    我们再回头看看第一种类型断言,它是 as const

    一般我们使用as后面跟的是一个数据类型比如string,number,any或interface

    使用as const是标准此变量是只读的。

    有些人会问不是已经使用了const定义变量了吗?此变量就是不能修改的呀!

    其实const定义的变量只是不能修改它自身,如果它是引用类型的变量,比如数组,对象,我们是可以修改它的子项的。

    const arr = [1];
    
    arr = [2]; // 此处会报错
    
    arr.push(2); 此处不会报错
    
    const obj = {
        a: 1
    };
    
    obj = {}; // 此处会报错
    
    obj.b = 2; // 此处不会报错
    

    但是在TypeScript中,使用as const后,变量就彻底修改不了

    const arr = [1] as const;
    arr.push(2); // Property 'push' does not exist on type 'readonly [1]'
    

    其他用法

    当我们调用了别人写的某个用JS写的方法时,它的返回值是一个不确定属性的对象,我们可以使用断言:

    // getParamsFromUrl.js 此文件使用JS编写
    export default function getParamsFromUrl (str) {
      const query = {}
      const url = str || window.location.href
      url.replace(/([^?&=]+)=([^?&=]+)/g, (a, b, c) => {
        query[b] = c ? decodeURIComponent(c) : ''
      })
      return query
    }
    
    // index.tsx
    import getParamsFromUrl from '@/utils/getParamsFromUrl';
    
    class CarInfo extends Component<PropsInterface, StateInterface> {
      componentDidMount () {
          // 使用断言
          let params = getParamsFromUrl() as { vehicleCode: string }
          requestQueryVehicle({
            vehicleCode: params.vehicleCode
          }).then((res: ResponseInterface<string>) => {
              // 其他代码...
          });
      }
    }
    

    如果不使用断言,TS会报错 Property 'vehicleCode' does not exist on type '{}',编译不通过。

    类型保护

    在文件packages/shared/src/index.ts中有大量类似如下代码

    export const isString = (val: unknown): val is string => typeof val === 'string'
    

    上面代码几个关键词:unknow、is、typeof

    unknow是TypeScript十三中基础数据类型之一,这个再下文单独说。

    先看is和typeof,他们即做到了类型保护。

    那什么是类型保护,为什么会出现类型保护呢?

    先看下面的例子

    interface Bird {
        fly: Function;
        layEggs: Function;
    }
    
    interface Fish {
        swim: Function;
        layEggs: Function;
    }
    const bird: Bird = {
        fly: () => {},
        layEggs: () => {}
    }
    const fish: Fish = {
        swim: () => {},
        layEggs: () => {}
    }
    
    
    function getSmallPet(name: 'fish' | 'bird'): Fish | Bird {
        if (name === 'fish') {
            return fish;
        }
        return bird;
    }
    
    let pet = getSmallPet('fish');
    pet.layEggs(); // okay
    pet.swim();    // errors
    

    因为pet可能是Fish类型也可能是Bird类型,而Bird类型是没有swim方法的,所以报错。

    那就需要判断pet的类型,可以使用上面说过的类型断言,修改代码如下:

    let pet = getSmallPet('fish');
    
    if ((pet as Fish).swim) {
        (pet as Fish).swim();
    }
    else {
        (pet as Bird).fly();
    }
    

    这里就有点恶心了,我们需要多次使用类型断言,否则编译不通过。

    如果有一种方案可以让我们一旦检查过类型,就能在之后的每个分支里清楚的知道pet的类型就好了。

    这个方案就是类型保护。

    类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。

    如何定义一个类型保护呢?

    很简单,只要简单地定义一个函数,它的返回值是一个类型谓词即val is string

    类型微辞的形式就是parameterName is Type,但parameterName必须是函数的参数。

    改写后的代码如下:

    function isFish(pet: Fish | Bird): pet is Fish {
        return (pet as Fish).swim !== undefined;
    }
    
    if (isFish(pet)) {
        pet.swim();
    }
    else {
        pet.fly();
    }
    

    这时,TypeScript不仅知道if分支里pet是Fish类型,还清楚在else分支里一定不是Fish类型而是Bird类型。

    至于typeof,很简单了,因为JS里也有这个关键词。

    只是在TypeScript里,它也是一种类型保护的方案,但是用法和JS没区别,这里就不细说了。

    还有一种类型保护就是instanceof,它是通过构造函数细化类型的一种方式。

    其实我们只需要记住类型谓词即可,typeof和instanceof就按照JS的用法就可以了。

    any

    在packages/shared/index.ts的第140,141行

    export const hasChanged = (value: any, oldValue: any): boolean =>
      value !== oldValue && (value === value || oldValue === oldValue)
    

    这里定义了value和oldValue的类型都是any。

    any是TypeScript基础类型之一,也是最灵活的类型。

    使用了any后,TypeScript类型检查器会跳过对这些值的检查,从而直接通过编译阶段。

    由于这个原因,强烈建议少使用any。

    而对于一些初学者,当遇到类型不能匹配的问题时,就会直接使用any代替,这是很不好的习惯。

    我们使用TypeScript就是想用它的类型检查,在编译阶段就能检查出一些问题,减少bug的出现,如果都用了any,那TypeScript将失去它的意义。

    any除了跳过类型检查外,我们再看看它还有哪些特性。

    隐试类型any

    当定义变量时,未指定其类型且未进行初始化赋值,TypeScript编译器不能推断出类型,默认即为any。

    let a; // 默认是any类型
    a = 1; // 可以赋值
    a = '闹闹前端';  // 可以赋值
    a = {}  // 可以赋值
    

    但是这种隐式any,还是有所受限的,比如不能访问它的任何属性

    a.setName('闹闹前端'); // ERROR let a: undefined Object is possibly 'undefined'.
    
    a(); // ERROR let a: undefined Cannot invoke an object which is possibly 'undefined'.
    

    显试类型any

    如果显试定义any类型,就不同了,我们可以访问任何属性,调用任何方法。

    无论这些属性和方法是否存在,TypeScript是不会检查他们是否存在和类型的。

    let a: any;
    a.setName('闹闹前端');
    a.firstName = '闹闹';
    a.name.lastName = '前端';
    
    // 取出a的属性,然后对b进行各种类型的赋值,也不会编译报错
    let b = a.b;
    b = 1;
    b = '1';
    b = {}
    
    let B: any;
    new B();
    

    以上TypeScript编译器都不会报出错误,会直接编译通过。

    上面的例子也能看出,对显试声明any类型的变量进行操作返回的内容也都是any类型。

    同时,any类型的变量可以赋值给任何类型的变量,除了never。

    let a: any;
    let b: number = a;
    let c: string = a;
    let d: any = a;
    

    以上都能通过编译。

    类型推断

    如果定义变量时,未指定类型但是进行初始化赋值了,TypeScript编译器会根据初始值推断出它的类型。

    let a = 1; // 推断出类型为number
    a = '闹闹前端'; //ERROR: let a: number  Type 'string' is not assignable to type 'number'.
    

    这样我们在编写代码时,对于简单明确类型的变量即可省略类型定义,使用类型推断。

    关闭隐试any

    我们可以在tsconfig.json中配置禁止隐试any的出现。

    "compilerOptions": {
        "noImplicitAny": true
      },
    

    这样,在有隐试any出现时,便会报出错误。

    unknown

    在pacakges/shared/index.ts中能看到很多函数参数类型为unknown例如:

    export const isString = (val: unknown): val is string => typeof val === 'string'
    

    unknown类型相对any更安全,虽然任何值都可以赋值给unknown,但是必须使用类型断言或类型保护或比较检查等细化到确认类型之后才能进行赋值,除了它自己和any外。

    同样,在unknown没有断言或细化到一个确认类型之前,是不允许对它进行任何操作的。

    比如上面的代码,是使用了typeof 进行了类型保护。

    值可以为任何类型

    let a: unknown = 1;
    a = '闹闹前端';
    a = {};
    

    它的值可以为任意类型。

    赋值给其他类型

    let a: unknown = 1;
    let b: number = a; // ERROR Type 'unknown' is not assignable to type 'number'.
    
    let c: number = a as number; // 正确 采用类型断言确定它的类型
    
    let d: number = typeof a === 'number' ? a : 1; // 正确 使用typeof类型保护确定它的类型
    
    if (a === 1) {  // 比较检查
        let b: number = a; // 正确 通过比较确定a的类型
    }
    
    if (a === '') {  比较检查
        let b: string = a; // 正确 通过比较确定a的类型
    }
    

    必须确定它的类型才能赋值给其他变量。

    void

    在pacakges/compiler-core/src/codegen.ts的第67行至第82行:

    export interface CodegenContext
      extends Omit<Required<CodegenOptions>, 'bindingMetadata'> {
      source: string
      code: string
      line: number
      column: number
      offset: number
      indentLevel: number
      pure: boolean
      map?: SourceMapGenerator
      helper(key: symbol): string
      push(code: string, node?: CodegenNode): void
      indent(): void
      deindent(withoutNewLine?: boolean): void
      newline(): void
    }
    

    这是一个自定义的interface,它继承了Omit。

    我们重点看它的属性push、indent、deindent、newline方法,他们都返回了void类型。

    void类型表示没有任何类型,几乎与any类型相反。

    它主要应用在函数返回值上,如果一个函数没有返回值时,它的返回类型就是void。

    这里要注意一点,函数没有返回值并不是真正的没有返回值,而是默认返回undefined。

    但是把返回类型为void的函数赋值给一个undefined类型的变量是不行的,虽然这样没有什么意义。

    function fn(): void {}
    let foo: undefined = fn(); // ERROR Type 'void' is not assignable to type 'undefined'
    
    let bar: null = fn(); // ERROR Type 'void' is not assignable to type 'null'
    

    不过,可以把undefined赋值给void类型的变量

    let a: void = undefined;
    let b: void = null; // 如果strictNullChecks为false
    

    never

    在packages/runtime-core/src/componentProps.ts的第63行至第65行;

    type PropMethod<T, TConstructor = any> = T extends (...args: any) => any // if is function with args
      ? { new (): TConstructor; (): T; readonly prototype: TConstructor } // Create Function like constructor
      : never
    

    这里使用了泛型,继承,readonly以及never。

    我们重点看never。

    never类型表示用不存在的值的类型。它是任何类型的子类型,可以赋值给任何类型;但是没有类型是never的子类型或可以赋值给never类型,除了never类型本身外,即使any也不可以,它就是这么倔强。

    它一般用在总是抛出异常或根本就不会有返回值的函数表达式或箭头函数的返回值类型。

    let a: never;
    a = 2; // ERROR Type 'number' is not assignable to type 'never'
    
    let fn = (): never => {throw new Error()}
    let b: number = 2;
    let c: string = '';
    b = fn(); // 正确,它是任何类型的子类型,可以赋值给任何类型
    c = fn(); // 正确,它是任何类型的子类型,可以赋值给任何类型
    

    下面返回never类型的函数

    // 返回never的函数必须存在无法达到的终点
    function throwError(error: string): never {
        throw new Error(error);
    }
    
    // 推断的返回值类型为never
    function reject() {
        return throwError('请求失败');
    }
    
    // 返回never的函数必须存在无法达到的终点
    function loop(): never {
        while(1) {}
    }
    

    Tuple 元组

    在文件packages/compiler-core/src/ast.ts的第466至第470行:

    export interface ListDynamicSlotNode extends CallExpression {
      callee: typeof RENDER_LIST
      arguments: [ExpressionNode, ListDynamicSlotIterator]
    }
    

    这里先定义了一个interface 其继承 CallExpression接口,成员arguments是一个Tuple类型即元组。

    定义元组类型后,可通过索引方式获取正确的类型。

    如果访问一个越界的元素,则会报错。

    给元组越界元素赋值,也会报错。

    举例如下:

    let tuple: [string, number] = ['闹闹前端', 100];
    let title = tuple[0]; // 正确获取
    let out = tuple[2]; // ERROR: Tuple type '[string, number]' of length '2' has no element at index '2'.
    
    tuple[2] = 200; // ERROR: Type '200' is not assignable to type 'undefined'.
    
    tuple[2] = undefined; // ERROR: Tuple type '[string, number]' of length '2' has no element at index '2'.
    
    

    Enum 枚举

    在文件packges/reactivity/src/operations.ts中

    export const enum TrackOpTypes {
      GET = 'get',
      HAS = 'has',
      ITERATE = 'iterate'
    }
    
    export const enum TriggerOpTypes {
      SET = 'set',
      ADD = 'add',
      DELETE = 'delete',
      CLEAR = 'clear'
    }
    

    在文件packges/runtime-core/src/hydration.ts中第28行至第32行

    const enum DOMNodeTypes {
      ELEMENT = 1,
      TEXT = 3,
      COMMENT = 8
    }
    

    在文件packages/runtime-core/src/errorHandling.ts中第8行至第24行

    export const enum ErrorCodes {
      SETUP_FUNCTION,
      RENDER_FUNCTION,
      WATCH_GETTER,
      WATCH_CALLBACK,
      WATCH_CLEANUP,
      NATIVE_EVENT_HANDLER,
      COMPONENT_EVENT_HANDLER,
      VNODE_HOOK,
      DIRECTIVE_HOOK,
      TRANSITION_HOOK,
      APP_ERROR_HANDLER,
      APP_WARN_HANDLER,
      FUNCTION_REF,
      ASYNC_COMPONENT_LOADER,
      SCHEDULER
    }
    

    这些都是枚举类型,关键字就是enum。

    上面举例Vue3代码的ErrorCodes就是从零开始编号,可以通过属性获取其值。

    let a = ErrorCodes.SETUP_FUNCTION;
    let b = ErrorCodes.RENDER_FUNCTION;
    let d = ErrorCodes.WATCH_GETTER;
    let e = ErrorCodes.WATCH_CALLBACK;
    

    编译后

    let a = 0 /* SETUP_FUNCTION */;
    let b = 1 /* RENDER_FUNCTION */;
    let d = 2 /* WATCH_GETTER */;
    let e = 3 /* WATCH_CALLBACK */;
    

    枚举的另一个好处就是,可以通过值获取当对应的名字。

    enum Color {
      Red = 1,
      Green,
      Blue,
    }
    let colorName: string = Color[2]; // Green;
    

    编译后

    "use strict";
    var Color;
    (function (Color) {
        Color[Color["Red"] = 1] = "Red";
        Color[Color["Green"] = 2] = "Green";
        Color[Color["Blue"] = 3] = "Blue";
    })(Color || (Color = {}));
    let colorName = Color[2];
    

    切记:如果想通过值获取对应的名字,不能添加const,否则会报错:A const enum member can only be accessed using a string literal.

    interface 接口

    interface 是经常使用的,在Vue3源码里到处可见interface。

    我们先看一个简单interface,在文件packages/runtime-core/src/components/Teleport.ts中第16行至第19行

    export interface TeleportProps {
      to: string | RendererElement | null | undefined
      disabled?: boolean
    }
    

    这里定义了TeleportProps接口,它包含一个必须项目to和可选项disabled。

    to类型可以是string或RendererElement或null或undefined。

    disabled类型是boolean。

    在比如文件packages/runtime-core/src/components/KeepAlive.ts中第50行至第60行

    export interface KeepAliveContext extends ComponentRenderContext {
      renderer: RendererInternals
      activate: (
        vnode: VNode,
        container: RendererElement,
        anchor: RendererNode | null,
        isSVG: boolean,
        optimized: boolean
      ) => void
      deactivate: (vnode: VNode) => void
    }
    

    定义了接口 KeepAliveContext 继承 ComponentRenderContext。 成员有类型为 RendererInternals 的renderer属性,类型为函数的 activate 熟悉,类型为函数 deactivate 的属性。

    属性 activate 和 deactivate 函数具体到了参数个数以及对应的类型和函数返回值。

    当我们声明变量使如果不符合其对应的规范,TypeScript就会报错。

    这里需要注意一下,函数参数仍然是形参,声明时不一定命名完全相同,但是一定要保证顺序以及类型是相同,否则会报错。

    举例如下:

    interface RenderInterface {
      render : (id: string, content: string) => string;
    }
    
    let render: RenderInterface = {
      render(root: string, innerHTML: string) {
        return `<div id="${root}">${innerHTML}</div>`;
      } 
    }
    render.render('root', 'Hello World!'); // <div id="root">Hello World!</div>
    

    我们再看看接口其他属性限制符。

    可选属性?

    如果接口的某个属性是可选的,只需要在可选属性名字定义的后面加上一个 ? 符合即可。

    比如上面接口 TeleportProps 的disabled属性。

    在使用可选熟悉时需要增加判断,否则TypeScript也会报错。

    我们把上面 RenderInterface 修改一下

    interface RenderInterface {
      render?: (id: string, content: string) => string;
    }
    
    let render: RenderInterface = {
      render(root: string, innerHTML: string) {
        return `<div id="${root}">${innerHTML}</div>`;
      } 
    }
    render.render('root', 'Hello World!'); // ERROR: (property) RenderInterface.render?: ((id: string, content: string) => string) | undefined
    // Cannot invoke an object which is possibly 'undefined'.(2722)
    

    可见报了一个属性可能为 undefined的错误,所以在使用之前要进行容错处理。

    只读属性 readonly

    一些对象属性只能在对象刚刚创建的时候修改其值。这时候就可以在属性名前添加 readonly 来指定只读属性。

    比如在文件packages/reactivity/src/computed.ts中第7行至第13行

    export interface ComputedRef<T = any> extends WritableComputedRef<T> {
      readonly value: T
    }
    
    export interface WritableComputedRef<T> extends Ref<T> {
      readonly effect: ReactiveEffect<T>
    }
    

    既然是只读的,在初始生命定义后,其值就确定了且以后都不可以修改。

    举例

    interface Point {
        readonly x: number;
        readonly y: number;
    }
    
    let p1: Point = { x: 10, y: 20 };
    p1.x = 5; // ERROR: Cannot assign to 'x' because it is a read-only property.
    

    TypeScript 有一个 ReadonlyArray<T> 类型,它确保了数组不能修改,此时和const有明确的区别。

    举例

    let a: number[] = [1, 2, 3, 4];
    let ro: ReadonlyArray<number> = a;
    ro[0] = 12; // ERROR: Index signature in type 'readonly number[]' only permits reading.
    ro.push(5); // ERROR: Property 'push' does not exist on type 'readonly number[]'.
    ro.length = 100; // ERROR: Cannot assign to 'length' because it is a read-only property.
    

    虽然我们不能修改数组成员以及属性,但是我们可以修改数组。

    ro = [3, 4, 5, 6]; // 正确
    

    但是如果使用 const 限制 ro 则不能直接修改它了。

    const ro = [1, 2, 3, 4];
    ro = [1, 2, 3, 4]; // ERROR: Cannot assign to 'ro' because it is a constant.
    

    额外属性

    我们经常并不知道一个接口的全部成员,或者成员的名称和类型,这时候我们就可以使用额外属性,使接口具有可扩展性。

    定义接口的额外属性如下:

    interface ListInterface {
        name: string;
        [propName: string]: any;
    }
    
    let list: ListInterface = {
        name: '闹闹前端',
        language: 'JavaScript',
        age: 1
    }; // 声明正确
    

    我们定义了具有额外属性的ListInterface接口,关键地方在

    [propName: string]: any;
    

    其可扩张属性名必须是string类型,其值可以为任何类型。

    当然如果可以确定属性值类型,也可以指定具体的类型。

    interface ListInterface {
        name: string;
        [propName: string]: string | number;
    }
    

    额外属性最大的使用场景就是在策略模式中。

    interface DirectionInterface {
      up: () => void;
      down: () => void;
      left: () => void;
      right: () => void;
    }
    const directMap: DirectionInterface = {
      up: () => {
        console.log('up');
      },
      down: () => {
        console.log('down');
      },
      left: () => {
        console.log('left');
      },
      right: () => {
        console.log('right');
      }
    };
    
    function move (direct: string) {
      const fn = directMap[direct]; // ERROR: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'DirectionInterface'.
      No index signature with a parameter of type 'string' was found on type 'DirectionInterface'.
      fn();
    }
    

    如果我们给 DirectionInterface 添加额外属性,就不会再报错了

    interface DirectionInterface {
      up: () => void;
      down: () => void;
      left: () => void;
      right: () => void;
      [propsName: string]: () => void;
    }
    const directMap: DirectionInterface = {
      up: () => {
        console.log('up');
      },
      down: () => {
        console.log('down');
      },
      left: () => {
        console.log('left');
      },
      right: () => {
        console.log('right');
      }
    };
    
    function move (direct: string) {
      const fn = directMap[direct]; // 正确
      fn();
    }
    

    当然这地有风险,万一direct不是up,down,left,right中的一个,那fn就是undefined了。

    最好的方式其实是定义move参数direct为 'up' | 'down' | 'left' | 'right'。

    这里只是举一个例子,以后在编程中使用策略模式遇到类似错误,可以按照这两种方案修改。

    继承

    接口可以继承。 在文件packages/compiler-core/src/ast.ts中第66行至第69行以及第100行至第112行

    export interface Node {
      type: NodeTypes
      loc: SourceLocation
    }
    
    export interface RootNode extends Node {
      type: NodeTypes.ROOT
      children: TemplateChildNode[]
      helpers: symbol[]
      components: string[]
      directives: string[]
      hoists: (JSChildNode | null)[]
      imports: ImportItem[]
      cached: number
      temps: number
      ssrHelpers?: symbol[]
      codegenNode?: TemplateChildNode | JSChildNode | BlockStatement | undefined
    }
    

    继承没什么特别的,大家应该都很熟悉了,就不细说了。

    类型别名

    在Vue3源码中我们到处可见如下代码

    type KeyedEvent = KeyboardEvent | MouseEvent | TouchEvent
    

    这行代码在文件pacakges/runtime-dom/src/directives/vOn.ts的第5行。

    type 就是类型别名关键字,

    这行代码的意思就是定义 KeyboardEvent | MouseEvent | TouchEvent 的联合类型的别名为 KeyedEvent 。

    再比如再文件packages/compiler-core/src/ast.ts的第85行至第99行

    export type ParentNode = RootNode | ElementNode | IfBranchNode | ForNode
    
    export type ExpressionNode = SimpleExpressionNode | CompoundExpressionNode
    
    export type TemplateChildNode =
      | ElementNode
      | InterpolationNode
      | CompoundExpressionNode
      | TextNode
      | CommentNode
      | IfNode
      | IfBranchNode
      | ForNode
      | TextCallNode
    

    这些都是定义类型别名。

    类型别名和接口interface很相似,几乎接口interface的所有功能都能用在类型别名type上。

    但是有一个细微的差别,那就是类型别名是不能扩展的。

    举例: 接口扩展

    interface ListInterface {
      name: string;
      id: string;
    }
    
    interface ListInterface {
      age: number;
    }
    
    // 正确
    let list: ListInterface = {
      name: '闹闹前端',
      id: 'naonaoFE',
      age: 1
    };
    

    类型别名扩展

    // ERROR: Duplicate identifier 'ListInterface'.
    type ListInterface = {
      name: string;
      id: string;
    }
    // ERROR: Duplicate identifier 'ListInterface'.
    type ListInterface = {
      age: number;
    }
    

    可见类型别名是不能扩展的。

    函数

    函数是JavaScript的一等公民,它是应用程序基础。

    TypeScript为函数添加了额外的功能,让我们更容易的使用。

    我们从Vue3源码中看看TypeScript的函数是如何定义的。

    在文件packages/reactivity/src/reactive.ts中第175行至第180行

    export function isReactive(value: unknown): boolean {
      if (isReadonly(value)) {
        return isReactive((value as Target)[ReactiveFlags.RAW])
      }
      return !!(value && (value as Target)[ReactiveFlags.IS_REACTIVE])
    }
    

    它定义了函数 isReactive ,参数value的类型是 unknown ,返回值类型是 boolean。

    这就是定义函数的基本语法糖。

    其实TypeScript可以根据返回语句自动推断出返回值类型,因此我们可以省略它。

    上面是定义一个函数,那如何定义一个函数类型呢?

    其实很简单,函数类型包含两部分:参数类型和返回值类型。

    所以我们可以这样定义函数类型:

    let add: (a: number, b: number) => number = function(x: number, y: number): number {
        return x + y;
    }
    

    可以给函数定义可选参数,与定义接口interface可选属性一样,在参数后加 ? 即可。

    比如在文件packages/compiler-core/src/errors.ts的第16至第30行

    export function createCompilerError<T extends number>(
      code: T,
      loc?: SourceLocation,
      messages?: { [code: number]: string },
      additionalMessage?: string
    ): T extends ErrorCodes ? CoreCompilerError : CompilerError {
      const msg =
        __DEV__ || !__BROWSER__
          ? (messages || errorMessages)[code] + (additionalMessage || ``)
          : code
      const error = new SyntaxError(String(msg)) as CompilerError
      error.code = code
      error.loc = loc
      return error as any
    }
    

    参数 loc 、 message、additionalMessage都是可选参数

    函数的默认参数和剩余参数都与ES6里相同,在此就不赘述了。

    TypeScript为函数提供了一个非常赞的功能就是函数重载。

    比如在文件packages/reactivity/src/reactive.ts的第62行至第74行

    export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
    export function reactive(target: object) {
      // if trying to observe a readonly proxy, return the readonly version.
      if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
        return target
      }
      return createReactiveObject(
        target,
        false,
        mutableHandlers,
        mutableCollectionHandlers
      )
    }
    

    这里就是定义了函数reactive的重载。

    它会根据传递的参数target的类型,返回不同的结果值。

    还在文件packages/reactivity/src/ref.ts得第24至第36行

    export function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
    export function isRef(r: any): r is Ref {
      return Boolean(r && r.__v_isRef === true)
    }
    
    export function ref<T extends object>(
      value: T
    ): T extends Ref ? T : Ref<UnwrapRef<T>>
    export function ref<T>(value: T): Ref<UnwrapRef<T>>
    export function ref<T = any>(): Ref<T | undefined>
    export function ref(value?: unknown) {
      return createRef(value)
    }
    

    这是对函数ref的重载。

    函数重载可以方便我们阅读函数参数个数即类型的可能性以及返回值的类型可能性,提高代码的可读性。

    泛型

    在上文中,讲函数类型时举例的代码中有

    export function createCompilerError<T extends number>
    
    export function reactive<T extends object>
    
    export function isRef<T>
    

    <T extends number><T extends object><T>就是泛型。前两个是用到了继承。

    举个简单的例子,我们工作中异步请求返回的数据结构基本是

    {
        code: 0,
        message: '成功'
        data: [
            {
                id: 1,
                name: '闹闹前端'
            },
            {
                id: 2,
                name: 'python'
            }
        ]
    }
    

    每个接口返回的data肯定是不同的,甚至data的数据类型可能是不同的。

    data 会根据业务需要,返回数组、字符串、布尔或者null。

    这时如果根据data类型书写interface就有所赘余。

    比如: data是json对象数组:

    interface ResponseDataItemInterface {
        id: number;
        name: string;
    }
    interface ResponseInterface {
        code: number;
        message: string;
        data: ResponseDataItemInterface[];
    }
    

    data是字符串:

    interface ResponseInterface {
        code: number;
        message: string;
        data: string;
    }
    

    我们可以用泛型合并上面两种interface

    interface ResponseInterface<T> {
        code: number;
        message: string;
        data: T;
    }
    

    在使用时指定类型就可以了

    requestList().then((res: ResponseInterface<ResponseDataItemInterface[]>) => {
        // ...
    })
    
    requestString().then((res: ResponseInterface<string>) => {
        // ...
    })
    
    requestNumber().then((res: ResponseInterface<number>) => {
        // ...
    })
    

    我们再看看上文举例Vue3源码中的 <T extends number>

    这是增加了泛型约束。

    举例 定义一个函数,参数必须有length属性。

    interface LengthInterface {
        length: number;
    }
    function lengthIdentity<T extends LengthInterface>(arg: T): T {
        arg.length = 0;
        return arg
    }
    lengthIdentity([]); // 正确
    lengthIdentity(''); // 正确
    lengthIdentity({length: 2, value: 3}); // 正确
    lengthIdentity(1); // ERROR: Argument of type 'number' is not assignable to parameter of type 'LengthInterface'.
    

    当我们想用属性名从对象里获取相应的属性,并确保这个属性名存在与对象上,该怎么办呢?

    TypeScript可以在泛型约束中使用类型参数,关键字就是 <K extends keyof T>

    比如在Vue3源码文件packages/reactivity/src/ref.ts的第167行至第174行

    export function toRef<T extends object, K extends keyof T>(
      object: T,
      key: K
    ): Ref<T[K]> {
      return isRef(object[key])
        ? object[key]
        : (new ObjectRefImpl(object, key) as any)
    }
    

    我们用一个具体的示例看一下效果

    function getProperty<T, K extends keyof T>(obj: T, key: K) {
      return obj[key];
    }
    
    let o = {
        a: 1,
        b: 2,
        c: 'c'
    };
    
    getProperty(o, 'a'); // 正确
    getProperty(o, 'c'); // 正确
    getProperty(o, 'd'); // ERROR: Argument of type '"d"' is not assignable to parameter of type '"c" | "a" | "b"'.
    

    是不是很方便,解决了隐藏的隐患。

    我们再看看packages/reactivity/src/ref.ts的第208行至第217行

    type UnwrapRefSimple<T> = T extends
      | Function
      | CollectionTypes
      | BaseTypes
      | Ref
      | RefUnwrapBailTypes[keyof RefUnwrapBailTypes]
      ? T
      : T extends Array<any>
        ? { [K in keyof T]: UnwrapRefSimple<T[K]> }
        : T extends object ? UnwrappedObject<T> : T
    

    这里竟然用到了三目运算符来确定UnwrapRefSimple的类型。

    这就是有条件类型。

    解析一下:

    第一步:如果T能赋值给Function、CollectionTypes、BaseTypes、Ref、RefUnwrapBailTypes[keyof RefUnwrapBailTypes]中的任何一个,那就用相应的类型。

    第二步:如果第一步不满足,那就看看如果T能赋值给数组类型,那T就是 { [K in keyof T]: UnwrapRefSimple<T[K]> }对象。

    第三步:如果第二步不满足,那就再看看如果T能赋值给object,那T就是UnwrappedObject<T>类型,否则就是T。

    这个稍微有点复杂,我们用一个简单的例子:

    type myType<T> = T extends string ? T : number;
    
    let a: myType<string> = '1'; // 正确
    let b: myType<object> = 2; // 正确
    let c: myType<object> = {}; // ERROR: Type '{}' is not assignable to type 'number'.
    

    解析: 自定义类型myType,如果T能赋值给string,那类型就是string,否则就是number。

    所以变量a和b的赋值都是正确的,变量c赋值报错了。

    在packages/reactivity/src/ref.ts的第204行至第207行

    export type UnwrapRef<T> = T extends Ref<infer V>
      ? UnwrapRefSimple<V>
      : UnwrapRefSimple<T>
    

    这里也是有条件类型,但是多了一个infer,它就是有条件类型的类型推断。

    我们再用举一个简单的示例

    type myType<T> = T extends (infer U)[] ? U : T;
    
    type T0 = myType<string>;
    type T1 = myType<number[]>;
    
    let a: T0 = '';
    let b: T1 = 1;
    

    解析:

    如果推断U数组类型,则T是U;否则就是T。

    T1类型是数字数组,则U是就是数字,所以变量b的类型就是数字。

    Partial<Type>

    在文件packages/runtime-core/src/componentPublicInstance.ts的第176行至178行

    $props: MakeDefaultsOptional extends true
        ? Partial<Defaults> & Omit<P & PublicProps, keyof Defaults>
        : P & PublicProps
    

    比如

    interface ListInterface {
        chargeNo: string;
        chargeType: 1 | 2 | 3;
    }
    type ListOptions = Partial<ListInterface>
    
    const list: ListInterface = {
        chargeNo: 'No.1',
        chargeType: 1
    };
    
    const listOptions: ListOptions = {
        chargeType: 2
    }
    

    这里的 ListOptions 等价于

    interface ListOptions {
        chargeNo?: string;
        chargeType?: 1 | 2 | 3;
    }
    

    不过,它是的作用是浅作用,只能作用在第一层上。 比如我们修改上面的代码

    interface SendInterface {
      quantity: number;
      weight: number;
      volume: number;
    }
    
    interface ListInterface {
        chargeNo: string;
        chargeType: 1 | 2 | 3;
        send: SendInterface;
    }
    
    type ListOptions = Partial<ListInterface>
    
    const list: ListInterface = {
        chargeNo: 'No.1',
        chargeType: 1,
        send: {
            quantity: 10,
            weight: 20,
            volume: 5
        }
    };
    
    const listOptions: ListOptions = {
        chargeType: 2,
        // ERROR: Type '{ quantity: number; }' is missing the following properties from type 'SendInterface': weight, volume
        send: {
            quantity: 10
        }
    };
    

    这时候变量 listOptions 的属性 send 便会报错。

    我们可以使用之前学习过的条件类型进行深度作用

    type DeepPartial<T> = {
         // 如果是 object,则递归类型
        [K in keyof T]?: T[K] extends object
          ? DeepPartial<T[K]>
          : T[K]
    };
    type ListOptions = DeepPartial<ListInterface>;
    

    这时候 listOptions 就不会报错了。

    完整代码如下

    interface SendInterface {
      quantity: number;
      weight: number;
      volume: number;
    }
    
    interface ListInterface {
        chargeNo: string;
        chargeType: 1 | 2 | 3;
        send: SendInterface;
    }
    
    const list: ListInterface = {
        chargeNo: 'No.1',
        chargeType: 1,
        send: {
            quantity: 10,
            weight: 20,
            volume: 5
        }
    };
    
    type DeepPartial<T> = {
         // 如果是 object,则递归类型
        [K in keyof T]?: T[K] extends object
          ? DeepPartial<T[K]>
          : T[K]
    };
    
    type ListOptions = DeepPartial<ListInterface>; 
    
    const listOptions: ListOptions = {
        chargeType: 2,
        send: {
            quantity: 10
        }
    };
    

    Required<Type>

    在文件packages/compiler-sfc/src/templateTransformSrcset.ts第30行至第35行

    export const createSrcsetTransformWithOptions = (
      options: Required<AssetURLOptions>
    ): NodeTransform => {
      return (node, context) =>
        (transformSrcset as Function)(node, context, options)
    }
    

    举例:

    interface RegisterInterface {
        userName: string;
        password: string;
        gender?: '男' | '女';
        age?: number;
    }
    type Register = Required<RegisterInterface>
    
    const register: Register = {
        userName: '闹闹前端',
        password: 'naonao',
        gender: '男',
        age: 2
    };
    
    // ERROR: Property 'age' is missing in type '{ userName: string; password: string; gender: "男"; }' but required in type 'Required<RegisterInterface>'
    const register1: Register = {
        userName: '闹闹前端',
        password: 'naonao',
        gender: '男'
    };
    

    它和Partial是相反的。

    Readonly<Type>

    在文件packages/runtime-core/src/componentSlots.ts第29行

    export type Slots = Readonly<InternalSlots>
    

    在文件packages/runtime-core/src/apiDefineComponent.ts第83行至第88行

    export function defineComponent<Props, RawBindings = object>(
      setup: (
        props: Readonly<Props>,
        ctx: SetupContext
      ) => RawBindings | RenderFunction
    ): DefineComponent<Props, RawBindings>
    

    举例:

    interface ListInterface {
        chargeNo: string;
        chargeType: 1 | 2 | 3;
    }
    
    const list: ListInterface = {
        chargeNo: 'No.1',
        chargeType: 1
    };
    
    // 编译正确
    list.chargeType = 2;
    
    type ReadOnlyListInterface = Readonly<ListInterface>;
    
    const readonlyList: ReadOnlyListInterface = {
        chargeNo: 'No.1',
        chargeType: 1
    };
    
    // ERROR: Cannot assign to 'chargeType' because it is a read-only property.
    readonlyList.chargeType = 2;
    

    当想对一个冻结对象的属性进行重新赋值时,它就非常有用了。

    Record<Keys,Type>

    在文件packages/compiler-core/src/compile.ts第20行至第22行

    export type TransformPreset = [
      NodeTransform[],
      Record<string, DirectiveTransform>
    ]
    

    在文件packages/compiler-core/src/parse.ts第36行至第42行

    const decodeMap: Record<string, string> = {
      gt: '>',
      lt: '<',
      amp: '&',
      apos: "'",
      quot: '"'
    }
    

    在文件packages/compiler-sfc/src/compileScript.ts第106行至108行

    const imports: Record<string, string> = {}
    const setupScopeVars: Record<string, boolean> = {}
    const setupExports: Record<string, boolean> = {}
    

    举例:

    interface SendInterface {
      weight: number;
      volume: number;
    }
    
    type CarName = 'car' | 'truck';
    
    type CarInterface = Record<CarName, SendInterface>;
    
    const car: CarInterface = {
        car: {
            weight: 10,
            volume: 5
        },
        truck: {
            weight: 20,
            volume: 10
        }
    }
    

    注意: Keys类型只能包含 number、string或symbol。

    type CarInterface = Record<CarName, SendInterface>;
    

    等价于

    interface CarInterface {
        car: SendInterface;
        truck: SendInterface;
    }
    

    Pick<Type, Keys>

    在文件packages/compiler-core/src/parse.ts第29行至第30行

    type MergedParserOptions = Omit<Required<ParserOptions>, OptionalOptions> &
      Pick<ParserOptions, OptionalOptions>
    

    举例:

    interface WaybillAuxiliaryListInterface {
      expectQuantity: number;
      height: number;
      length: number;
      sendSingleQuantity: number;
      skuCode: string;
      skuName: string;
      volume: number;
      weight: number;
      width: number;
    }
    
    // 从WaybillAuxiliaryListInterface中提取 weight和volume 构建新的类型
    type WeightVolume = Pick<WaybillAuxiliaryListInterface, 'weight' | 'volume'>;
    
    // 从WaybillAuxiliaryListInterface中提取 width和height 构建新的类型
    type WidthHeight = Pick<WaybillAuxiliaryListInterface, 'width' | 'height'>;
    
    const weightVolume: WeightVolume = {
        weight: 10,
        volume: 20
    };
    
    const widthHeight: WidthHeight = {
        width: 20,
        height: 20
    };
    

    其实

    type WeightVolume = Pick<WaybillAuxiliaryListInterface, 'weight' | 'volume'>;
    

    等价于

    interface WeightVolume {
        weight: number;
        volume: number;
    }
    

    它的道理和lodash里的 pick方法相同。

    Omit<Type, Keys>

    在文件packages/runtime-dom/src/nodeOps.ts第10行

    export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
      insert: (child, parent, anchor) => {
        // ...省略
      },
    
      remove: child => {
        // ...省略
      },
    
      // ...省略
    }
    

    在文件packages/runtime-dom/src/components/TransitionGroup.ts第34行至第37行

    export type TransitionGroupProps = Omit<TransitionProps, 'mode'> & {
      tag?: string
      moveClass?: string
    }
    

    举例:

    interface WaybillAuxiliaryListInterface {
      expectQuantity: number;
      height: number;
      length: number;
      sendSingleQuantity: number;
      skuCode: string;
      skuName: string;
      volume: number;
      weight: number;
      width: number;
    }
    // 排除WaybillAuxiliaryListInterface中的weight和volume外的其他属性,组合成新的类型
    type List = Omit<WaybillAuxiliaryListInterface, 'weight' | 'volume'>;
    
    // List类型不包含weight和volume
    const list: List = {
        expectQuantity: 10,
        height: 10,
        length: 10,
        sendSingleQuantity: 10,
        skuCode: 'No1',
        skuName: '商品',
        width: 10,
    };
    

    其实

    type List = Omit<WaybillAuxiliaryListInterface, 'weight' | 'volume'>;
    

    等价于

    interface List {
      expectQuantity: number;
      height: number;
      length: number;
      sendSingleQuantity: number;
      skuCode: string;
      skuName: string;
      width: number;
    }
    

    它的道理和lodash里的 omit方法相同。

    Exclude<Type, ExcludedUnion>

    在文件packages/runtime-core/src/componentProps.ts第71行

    type OptionalKeys<T> = Exclude<keyof T, RequiredKeys<T>>
    

    举例:

    interface Id {
        id: number;
    }
    interface Name {
        name: string;
    }
    interface Age {
        age: number;
    }
    // 从联合类型Id | Name | Age冲排除Age,剩余的组成新的联合类型
    type Info = Exclude<Id | Name | Age, Age>;
    
    const info1: Info = {
        id: 1
    };
    const info2: Info = {
        name: '前端'
    };
    

    其实

    type Info = Exclude<Id | Name | Age, Age>;
    

    等价于

    type Info = Id | Name;
    

    Extract<Type, Union>

    在文件packages/runtime-dom/types/jsx.d.ts第1307行

    type StringKeyOf<T> = Extract<keyof T, string>
    

    举例:

    interface Id {
        id: number;
    }
    interface Name {
        name: string;
    }
    interface Age {
        age: number;
    }
    type Info = Extract<Id | Name | Age, Age>;
    
    const info1: Info = {
        age: 1
    };
    const info2: Info = {
        age: 2
    };
    

    其实

    type Info = Extract<Id | Name | Age, Age>;
    

    等价于

    type Info = Age;
    

    Parameters<Type>

    在文件packages/runtime-core/src/vnode.ts第290行至297行

    const createVNodeWithArgsTransform = (
      ...args: Parameters<typeof _createVNode>
    ): VNode => {
      return _createVNode(
        ...(vnodeArgsTransformer
          ? vnodeArgsTransformer(args, currentRenderingInstance)
          : args)
      )
    }
    

    举例:

    function add(a: number, b: number): number {
        return a + b;
    }
    
    type AddArgs = Parameters<typeof add>;
    
    const addArgs: AddArgs = [1, 2];
    
    // ERROR: Type 'string' is not assignable to type 'number'.
    const addArgs1: AddArgs = ['1', 2];
    
    // ERROR: Type '[number, number, number]' is not assignable to type '[a: number, b: number]'.
      Source has 3 element(s) but target allows only 2
    const addArgs1: AddArgs = [1, 2, 3];
    

    ReturnType<Type>

    在文件packages/runtime-core/src/renderer.ts第2227行至第2228行

    let hydrate: ReturnType<typeof createHydrationFunctions>[0] | undefined
    let hydrateNode: ReturnType<typeof createHydrationFunctions>[1] | undefined
    

    举例:

    // 返回类型为number,则AddReturnType即为number;
    type AddReturnType = ReturnType<() => number>;
    // 返回类型为number,所以只能赋值数字
    const addReturnType: AddReturnType = 3;
    
    
    // 返回类型为{ a: string; b: number; }则AddReturnType即为{ a: string; b: number; }
    type AddReturnType = ReturnType<() => {a: string; b: number;}>;
    
    const addReturnType: AddReturnType = {
        a: 'a',
        b: 1
    };
    

    说在最后

    本文内容很长,需要一定耐心。

    学习本来就是一件很孤独的事,要耐住寂寞,扛住煎熬,才能让我们有所进步。

    最后如果喜欢本篇内容,欢迎关注微信公众号:闹闹前端。


    下载网 » 跟Vue3源码学习TypeScript

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元