美文网首页让前端飞TypeScript
巧用 TypeScript (一)

巧用 TypeScript (一)

作者: 三毛丶 | 来源:发表于2018-10-07 22:27 被阅读0次

    以下问题来自于与公司小伙伴以及网友的讨论,整理成章,希望提供另一种思路(避免踩坑)解决问题。

    函数重载

    TypeScript 提供函数重载的功能,用来处理因函数参数不同而返回类型不同的使用场景,使用时,只需为同一个函数定义多个类型即可,简单使用如下所示:

    declare function test(a: number): number;
    declare function test(a: string): string;
    
    const resS = test('Hello World');  // resS 被推断出类型为 string;
    const resN = test(1234);           // resN 被推断出类型为 number;
    

    它也适用于参数不同,返回值类型相同的场景,我们只需要知道在哪种函数类型定义下能使用哪些参数即可。

    考虑如下例子:

    interface User {
      name: string;
      age: number;
    }
    
    declare function test(para: User | number, flag?: boolean): number;
    

    在这个 test 函数里,我们的本意可能是当传入参数 paraUser 时,不传 flag,当传入 paranumber 时,传入 flag。TypeScript 并不知道这些,当你传入 paraUser 时,flag 同样允许你传入:

    const user = {
      name: 'Jack',
      age: 666
    }
    
    // 没有报错,但是与想法违背
    const res = test(user, false);
    

    使用函数重载能帮助我们实现:

    interface User {
      name: string;
      age: number;
    }
    
    declare function test(para: User): number;
    declare function test(para: number, flag: boolean): number;
    
    const user = {
      name: 'Jack',
      age: 666
    };
    
    // bingo
    // Error: 参数不匹配
    const res = test(user, false);
    

    实际项目中,你可能要多写几步,如在 class 中:

    interface User {
      name: string;
      age: number;
    }
    
    const user = {
      name: 'Jack',
      age: 123
    };
    
    class SomeClass {
    
      /**
       * 注释 1
       */
      public test(para: User): number;
      /**
       * 注释 2
       */
      public test(para: number, flag: boolean): number;
      public test(para: User | number, flag?: boolean): number {
        // 具体实现
        return 11;
      }
    }
    
    const someClass = new SomeClass();
    
    // ok
    someClass.test(user);
    someClass.test(123, false);
    
    // Error
    someClass.test(123);
    someClass.test(user, false);
    

    映射类型

    自从 TypeScript 2.1 版本推出映射类型以来,它便不断被完善与增强。在 2.1 版本中,可以通过 keyof 拿到对象 key 类型, 内置 PartialReadonlyRecordPick 映射类型;2.3 版本增加 ThisType ;2.8 版本增加 ExcludeExtractNonNullableReturnTypeInstanceType;同时在此版本中增加条件类型与增强 keyof 的能力;3.1 版本支持对元组与数组的映射。这些无不意味着映射类型在 TypeScript 有着举足轻重的地位。

    其中 ThisType 并没有出现在官方文档中,它主要用来在对象字面量中键入 this

    // Compile with --noImplicitThis
    
    type ObjectDescriptor<D, M> = {
      data?: D;
      methods?: M & ThisType<D & M>;  // Type of 'this' in methods is D & M
    }
    
    function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
      let data: object = desc.data || {};
      let methods: object = desc.methods || {};
      return { ...data, ...methods } as D & M;
    }
    
    let obj = makeObject({
      data: { x: 0, y: 0 },
      methods: {
        moveBy(dx: number, dy: number) {
          this.x += dx;  // Strongly typed this
          this.y += dy;  // Strongly typed this
        }
      }
    });
    
    obj.x = 10;
    obj.y = 20;
    obj.moveBy(5, 5);
    

    正是由于 ThisType 的出现,Vue 2.5 才得以增强对 TypeScript 的支持。

    虽已内置了很多映射类型,但在很多时候,我们需要根据自己的项目自定义映射类型:

    比如你可能想取出接口类型中的函数类型:

    type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
    type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;
    
    interface Part {
      id: number;
      name: string;
      subparts: Part[];
      updatePart(newName: string): void;
    }
    
    type T40 = FunctionPropertyNames<Part>;  // "updatePart"
    type T42 = FunctionProperties<Part>;     // { updatePart(newName: string): void }
    

    比如你可能为了便捷,把本属于某个属性下的方法,通过一些方式 alias 到其他地方。

    举个例子:SomeClass 下有个属性 value = [1, 2, 3],你可能在 Decorators 给类添加了此种功能:在 SomeClass 里调用 this.find() 时,实际上是调用 this.value.find(),但是此时 TypeScript 并不知道这些:

    class SomeClass {
      value = [1, 2, 3];
    
      someMethod() {
        this.value.find(/* ... */);  // ok
        this.find(/* ... */);        // Error:SomeClass 没有 find 方法。
      }
    }
    

    借助于映射类型,和 interface + class 的声明方式,可以实现我们的目的:

    type ArrayMethodName = 'filter' | 'forEach' | 'find';
    
    type SelectArrayMethod<T> = {
     [K in ArrayMethodName]: Array<T>[K]
    }
    
    interface SomeClass extends SelectArrayMethod<number> {}
    
    class SomeClass {
     value = [1, 2, 3];
    
     someMethod() {
       this.forEach(/* ... */)        // ok
       this.find(/* ... */)           // ok
       this.filter(/* ... */)         // ok
       this.value                     // ok
       this.someMethod()              // ok
     }
    }
    
    const someClass = new SomeClass();
    someClass.forEach(/* ... */)        // ok
    someClass.find(/* ... */)           // ok
    someClass.filter(/* ... */)         // ok
    someClass.value                     // ok
    someClass.someMethod()              // ok
    

    导出 SomeClass 类时,也能使用。

    可能有点不足的地方,在这段代码里 interface SomeClass extends SelectArrayMethod<number> {} 你需要手动添加范型的具体类型(暂时没想到更好方式)。

    类型断言

    类型断言用来明确的告诉 TypeScript 值的详细类型,合理使用能减少我们的工作量。

    比如一个变量并没有初始值,但是我们知道它的类型信息(它可能是从后端返回)有什么办法既能正确推导类型信息,又能正常运行了?有一种网上的推荐方式是设置初始值,然后使用 typeof 拿到类型(可能会给其他地方用)。然而我可能比较懒,不喜欢设置初始值,这时候使用类型断言可以解决这类问题:

    interface User {
      name: string;
      age: number;
    }
    
    export default class NewRoom extends Vue {
      private user = {} as User;
    }
    

    在设置初始化时,添加断言,我们就无须添加初始值,编辑器也能正常的给予代码提示了。如果 user 属性很多,这样就能解决大量不必要的工作了,定义的 interface 也能给其他地方使用。

    枚举类型

    枚举类型分为数字类型与字符串类型,其中数字类型的枚举可以当标志使用:

    // https://github.com/Microsoft/TypeScript/blob/master/src/compiler/types.ts#L3859
    export const enum ObjectFlags {
      Class            = 1 << 0,  // Class
      Interface        = 1 << 1,  // Interface
      Reference        = 1 << 2,  // Generic type reference
      Tuple            = 1 << 3,  // Synthesized generic tuple type
      Anonymous        = 1 << 4,  // Anonymous
      Mapped           = 1 << 5,  // Mapped
      Instantiated     = 1 << 6,  // Instantiated anonymous or mapped type
      ObjectLiteral    = 1 << 7,  // Originates in an object literal
      EvolvingArray    = 1 << 8,  // Evolving array type
      ObjectLiteralPatternWithComputedProperties = 1 << 9,  // Object literal pattern with computed properties
      ContainsSpread   = 1 << 10, // Object literal contains spread operation
      ReverseMapped    = 1 << 11, // Object contains a property from a reverse-mapped type
      JsxAttributes    = 1 << 12, // Jsx attributes type
      MarkerType       = 1 << 13, // Marker type used for variance probing
      JSLiteral        = 1 << 14, // Object type declared in JS - disables errors on read/write of nonexisting members
      ClassOrInterface = Class | Interface
    }
    

    在 TypeScript src/compiler/types 源码里,定义了大量如上所示的基于数字类型的常量枚举。它们是一种有效存储和表示布尔值集合的方法

    《深入理解 TypeScript》 中有一个使用例子:

    enum AnimalFlags {
      None        = 0,
      HasClaws    = 1 << 0,
      CanFly      = 1 << 1,
      HasClawsOrCanFly = HasClaws | CanFly
    }
    
    interface Animal {
      flags: AnimalFlags;
      [key: string]: any;
    }
    
    function printAnimalAbilities(animal: Animal) {
      var animalFlags = animal.flags;
      if (animalFlags & AnimalFlags.HasClaws) {
        console.log('animal has claws');
      }
      if (animalFlags & AnimalFlags.CanFly) {
        console.log('animal can fly');
      }
      if (animalFlags == AnimalFlags.None) {
        console.log('nothing');
      }
    }
    
    var animal = { flags: AnimalFlags.None };
    printAnimalAbilities(animal); // nothing
    animal.flags |= AnimalFlags.HasClaws;
    printAnimalAbilities(animal); // animal has claws
    animal.flags &= ~AnimalFlags.HasClaws;
    printAnimalAbilities(animal); // nothing
    animal.flags |= AnimalFlags.HasClaws | AnimalFlags.CanFly;
    printAnimalAbilities(animal); // animal has claws, animal can fly
    

    上例代码中 |= 用来添加一个标志,&=~ 用来删除标志,| 用来合并标志。

    相关文章

      网友评论

        本文标题:巧用 TypeScript (一)

        本文链接:https://www.haomeiwen.com/subject/vqlfaxtx.html