TS学习要点

Untitled

最怕学偏了

Untitled

  1. 用好类型,不要魔化
  2. 用好OO,这才是TS的另一个优势
  3. Hack的东西少用,可维护性更重要

类型基础

以下是一些TypeScript独有的语法和特性:

  1. 类型注解:TypeScript可以为变量、函数参数、函数返回值等添加类型注解,以指定它们的数据类型。例如:

    let age: number = 25;
    function add(a: number, b: number): number {
      return a + b;
    }
    
  2. 接口:TypeScript支持接口的定义,用于描述对象的结构和行为。接口可以定义属性、方法、可选属性、只读属性等。例如:

    interface Person {
      name: string;
      age: number;
      sayHello(): void;
    }
    
  3. 泛型:TypeScript支持泛型,用于创建可重用的、类型安全的代码。泛型可以在函数、类、接口中使用,以实现对不同类型的支持。例如:

    function identity<T>(arg: T): T {
      return arg;
    }
    let result = identity<number>(10);
    
  4. 类型别名和联合类型:TypeScript支持类型别名,用于给一个类型起一个新的名字。它还支持联合类型,用于指定一个变量可以是多个类型中的一个。例如:

    type Point = {
      x: number;
      y: number;
    };
    type Shape = Circle | Rectangle | Triangle;
    
  5. 枚举:TypeScript支持枚举类型,用于定义一组具名的常量。枚举类型可以是数字枚举或字符串枚举。例如:

    enum Color {
      Red,
      Green,
      Blue
    }
    let color: Color = Color.Red;
    

这些是TypeScript独有的一些语法和特性,它们使得TypeScript相比于JavaScript具有更强大的类型检查和面向对象编程的能力。通过使用这些特性,开发者可以更好地组织和管理代码,提高代码的可维护性和可读性。

为了让大家能更好地理解并掌握 TypeScript 内置类型别名,我们先来介绍一下相关的一些基础知识。

  1. typeof

    在 TypeScript 中,typeof 操作符可以用来获取一个变量声明或对象的类型。

    interface Person {
      name: string;
      age: number;
    }
    
    const sem: Person = { name: 'semlinker', age: 30 };
    type Sem= typeof sem;// -> Person
    
    function toArray(x: number): Array<number> {
      return [x];
    }
    
    type Func = typeof toArray;// -> (x: number) => number[]
    
  2. keyof

    keyof 操作符可以用来获取一个对象中的所有 key 值:

    interface Person {
    name: string;
    age: number;
    }
    
    type K1 = keyof Person;// "name" | "age"
    type K2 = keyof Person[];// "length" | "toString" | "pop" | "push" | "concat" | "join"
    type K3 = keyof { [x: string]: Person };// string | number
    
  3. in

    in 用来遍历枚举类型:

    type Keys = "a" | "b" | "c"
    
    type Obj =  {
      [p in Keys]: any
    }// -> { a: any, b: any, c: any }
    
  4. infer

    在条件类型语句中,可以用 infer 声明一个类型变量并且对它进行使用。

    type ReturnType<T> = T extends (
      ...args: any[]
    ) => infer R ? R : any;
    

    以上代码中 infer R 就是声明一个变量来承载传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用。

    结合下面这种例子,比较容易理解,提取数组的类型。

    type T0 = string[];
    type T1 = number[];
    
    type UnpackedArray<T> = T extends (infer U)[] ? U : T
    type U0 = UnpackedArray<T1> // number
    
    let a:U0 = 1
    

    变化一下,如果UnpackedArray参数没有类型,此时的a就1或2,这就是T存在的意义的。

    type T0 = string[];
    type T1 = number[];
    type T2 = [1,2];
    
    type UnpackedArray<T> = T extends (infer U)[] ? U : T
    type U0 = UnpackedArray<T2> // number
    
    let a:U0 = 1
    

    提取数组里的第一个元素,也可以这样做。

    type First<T extends any[]> = T extends [infer A, ...infer test] ? A : never;
    

    还有更复杂的例子,简单的Includes

    type Includes<T extends readonly any[], U> = U extends T[number] ? true : false;
    

    Untitled

    如果数组里,含有对象、数组呢?通过 extends + infer + rest + recursive 可以 loop Tuple 返回一个 Type。

    type Includes<T extends readonly any[], U> = T extends [infer First, ...infer Rest]
    ? Equal<U, First> extends true
      ? true
      : Includes<Rest, U>
    : false;
    

    这样玩下去就很有意思了

  5. extends

    有时候我们定义的泛型不想过于灵活或者说想继承某些类等,可以通过 extends 关键字添加泛型约束。

    interface ILengthwise {
      length: number;
    }
    
    function loggingIdentity<T extends ILengthwise>(arg: T): T {
      console.log(arg.length);
      return arg;
    }
    

    现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:

    loggingIdentity(3);// Error, number doesn't have a .length property
    

    这时我们需要传入符合约束类型的值,必须包含必须的属性:

    loggingIdentity({length: 10, value: 3});
    

    掌握了这些内容,再看一些https://github.com/type-challenges/type-challenges/blob/main/utils/index.d.ts就容易多了

    export type Expect<T extends true> = T
    export type ExpectTrue<T extends true> = T
    export type ExpectFalse<T extends false> = T
    export type IsTrue<T extends true> = T
    export type IsFalse<T extends false> = T
    
    export type Equal<X, Y> =
      (<T>() => T extends X ? 1 : 2) extends
      (<T>() => T extends Y ? 1 : 2) ? true : false
    export type NotEqual<X, Y> = true extends Equal<X, Y> ? false : true
    
    // https://stackoverflow.com/questions/49927523/disallow-call-with-any/49928360#49928360
    export type IsAny<T> = 0 extends (1 & T) ? true : false
    export type NotAny<T> = true extends IsAny<T> ? false : true
    
    export type Debug<T> = { [K in keyof T]: T[K] }
    export type MergeInsertions<T> =
      T extends object
        ? { [K in keyof T]: MergeInsertions<T[K]> }
        : T
    
    export type Alike<X, Y> = Equal<MergeInsertions<X>, MergeInsertions<Y>>
    
    export type ExpectExtends<VALUE, EXPECTED> = EXPECTED extends VALUE ? true : false
    export type ExpectValidArgs<FUNC extends (...args: any[]) => any, ARGS extends any[]> = ARGS extends Parameters<FUNC>
      ? true
      : false
    
    export type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never
    

    以Equal举例

    大佬 @mattmccutchen 给出了一个非常精彩的解决方案

    Here's a solution that makes creative use of the assignability rule for conditional types, which requires that the types after

    Here's a solution that makes creative use of the assignability rule for conditional types, which requires that the types after extends be "identical" as that is defined by the checker:
    ts export type Equals<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
    This passes all the tests from the initial description that I was able to run except H, which fails because the definition of "identical" doesn't allow an intersection type to be identical to an object type with the same properties. (I wasn't able to run test E because I don't have the definition of Head.)
    

    参考:https://zhuanlan.zhihu.com/p/597298193

面向对象

TypeScript是JavaScript的超集,它在JavaScript的基础上扩展了一些独有的语法和特性,以提供更强大的类型检查和面向对象编程的支持。

从Java过来的同学,会发现这些概念和Java是类似的,不过ts的语法更加简单。

  • get set 竟然是关键字,后面可直接跟上函数。可以改变属性的赋值和读取行为!
  • staticinstanceofpublicprotectedprivate这些也都是有的,真的感觉和写Java没什么两样
  • constructor 默认是构造方法,不像是Java要和class的名词一样
  • abstract 也有,表明子类必须实现,没什么两样
  • 关于类和接口的区别,我觉得熟悉java的,对ts来说就是透明的
  • 范型在Java里,语法也是非常的变态,因为你很多时候不知道要把<>放在什么地方。在ts中,一样的难受。具体怎么熟悉,只有在实践中磨练了

下面是一段简单的ts代码,可以很好的表达面向对象的写法。

class Animal {
  public name;
  protected a;
  private b: string;
  constructor(name) {
    this.name = name;
  }
  get name() {
   return 'Jack';
  }
  set name(value) {
    console.log('setter: ' + value);
  }
  sayhi() {
    return `my name is ${this.name}`;
  }
}

class Cat extends Animal {
  constructor(name) {
    super(name)
  }
  sayhi() {
    return "meow " + super.sayhi()
  }
  static iaAnimal(a) {
    return a instanceof Animal;
  }
}

function gen<T extends Animal>(name: T): void {
  console.log(name.name)
}

虽然 JavaScript 中有类的概念,但是可能大多数 JavaScript 程序员并不是非常熟悉类,这里对类相关的概念做一个简单的介绍。

  • 类(Class):定义了一件事物的抽象特点,包含它的属性和方法
  • 对象(Object):类的实例,通过 new 生成
  • 存取器(getter & setter):用以改变属性的读取和赋值行为
  • 修饰符(Modifiers):修饰符是一些关键字,用于限定成员或类型的性质。比如 public 表示公有属性或方法
  • 抽象类(Abstract Class):抽象类是供其他类继承的基类,抽象类不允许被实例化。抽象类中的抽象方法必须在子类中被实现
  • 接口(Interfaces):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(implements)。一个类只能继承自另一个类,但是可以实现多个接口

面向对象基础

面向对象(OOP)的三大特性:封装、继承、多态

  • 封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,同时也保证了外界无法任意更改对象内部的数据
  • 继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性
  • 多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如 Cat 和 Dog 都继承自 Animal,但是分别实现了自己的 eat 方法。此时针对某一个实例,我们无需了解它是 Cat 还是 Dog,就可以直接调用 eat 方法,程序会自动判断出来应该如何执行 eat

装饰器和IoC

举例Midway或Nestjs,他们都属于同一类的web框架

控制器

import { Controller, Get } from '@nestjs/common';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}

服务层

import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  create(cat: Cat) {
    this.cats.push(cat);
  }

  findAll(): Cat[] {
    return this.cats;
  }
}

实现要点

  1. 装饰器获取元数据
  2. 通过 inversify 或 typedi 等基于typescript的ioc框架,结合元数据实现。

再举个例子,这是java里的测试框架JUnit的写法。

class MyFirstJUnitJupiterTests {

  private final Calculator calculator = new Calculator();

  @Test
  void addition() {
    assertEquals(2, calculator.add(1, 1));
  }
}

放到ts世界,使用类似于midway的写法。

  1. 使用注解

    class MyFirstJUnitJupiterTests {
      private calculator = new Calculator();
    
      @Test
      addition() {
        assert.is(2, this.calculator.add(1, 1));
      }
    }
    
  2. 使用ioc容器注入

    class MyFirstJUnitJupiterTests {
      @Inject()
      calculator: Calculator;
    
      @Test
      addition() {
        assert.is(2, this.calculator.add(1, 1));
      }
    }
    

    这里的@Test实现如下。

    export function Test(
      target: object | any,
      propertyName: string,
      descriptor: TypedPropertyDescriptor<any>,
    ) {
      debug(target[propertyName] + descriptor);
    
      //classname
      const className = target.constructor.name;
    
      if (!cache[className]) cache[className] = {};
      if (!cache[className][propertyName]) cache[className][propertyName] = {};
    
      cache[className][propertyName]["desc"] = "no display name";
      cache[className][propertyName]["fn"] = target[propertyName];
    }
    

其实,说白了就是通过装饰器,获取测试用例信息,等执行测试的时候再去调用。

设计模式

设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

Untitled

  • 5种(创建型模式):工厂方法模式、抽象工厂模式、单例模式、原型模式、建造者模式。
  • 7种(结构型模式):适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
  • 11种(行为型模式):策略模式、模板方法模式、观察者模式、迭代器模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

本课程作为入门课程,不做深入探讨,以模板模式举例,让大家能够了解什么是设计模式和大致应用即可。

模板方法模式(Template Method Pattern),又叫模板模式(Template Pattern),在一个抽象类公开定义了执行它的方法的模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。

简单说,模板方法模式,定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构,就可以重定义该算法的某些特定步骤,这种类型的设计模式属于行为型模式。

用abstract定义抽象类和抽象方法,抽象类中的抽象方法不包含具体实现并且必须在派生类中实现

  1. 抽象方法必须在抽象类中

  2. 抽象类和抽象方法是一个标准,定义标准后,子类中必须包含抽象定义的方法

abstract class Father { /* 定义一个抽象方法 */
  public age: number;
  constructor(age: number) {
    this.age = age
  }
  abstract counts(): any; /* 抽象方法必须在抽象类中 */
}

class children extends Father {
  constructor(age: number) {
    super(age)
  }
  counts(): void {    /* 子类中必须有抽象类中的抽象方法 */
    console.log(this.age - 1)
  }
}

举例模版模式

/**
 * The Abstract Class defines a template method that contains a skeleton of some
 * algorithm, composed of calls to (usually) abstract primitive operations.
 *
 * Concrete subclasses should implement these operations, but leave the template
 * method itself intact.
 */
abstract class AbstractClass {
  /**
   * The template method defines the skeleton of an algorithm.
   */
  public templateMethod(): void {
    this.baseOperation1();
    this.requiredOperations1();
    this.baseOperation2();
    this.hook1();
    this.requiredOperation2();
    this.baseOperation3();
    this.hook2();
  }

  /**
   * These operations already have implementations.
   */
  protected baseOperation1(): void {
    console.log('AbstractClass says: I am doing the bulk of the work');
  }

  protected baseOperation2(): void {
    console.log('AbstractClass says: But I let subclasses override some operations');
  }

  protected baseOperation3(): void {
    console.log('AbstractClass says: But I am doing the bulk of the work anyway');
  }

  /**
   * These operations have to be implemented in subclasses.
   */
  protected abstract requiredOperations1(): void;

  protected abstract requiredOperation2(): void;

  /**
   * These are "hooks." Subclasses may override them, but it's not mandatory
   * since the hooks already have default (but empty) implementation. Hooks
   * provide additional extension points in some crucial places of the
   * algorithm.
   */
  protected hook1(): void { }

  protected hook2(): void { }
}

/**
 * Concrete classes have to implement all abstract operations of the base class.
 * They can also override some operations with a default implementation.
 */
class ConcreteClass1 extends AbstractClass {
  protected requiredOperations1(): void {
    console.log('ConcreteClass1 says: Implemented Operation1');
  }

  protected requiredOperation2(): void {
    console.log('ConcreteClass1 says: Implemented Operation2');
  }
}

/**
 * Usually, concrete classes override only a fraction of base class' operations.
 */
class ConcreteClass2 extends AbstractClass {
  protected requiredOperations1(): void {
    console.log('ConcreteClass2 says: Implemented Operation1');
  }

  protected requiredOperation2(): void {
    console.log('ConcreteClass2 says: Implemented Operation2');
  }

  protected hook1(): void {
    console.log('ConcreteClass2 says: Overridden Hook1');
  }
}

/**
 * The client code calls the template method to execute the algorithm. Client
 * code does not have to know the concrete class of an object it works with, as
 * long as it works with objects through the interface of their base class.
 */
function clientCode(abstractClass: AbstractClass) {
  // ...
  abstractClass.templateMethod();
  // ...
}

console.log('Same client code can work with different subclasses:');
clientCode(new ConcreteClass1());
console.log('');

console.log('Same client code can work with different subclasses:');
clientCode(new ConcreteClass2());

还以测试举例,测试框架其实有很多种。如果写法都一样,但底层想使用mocha或jest实现,是不是可以使用模板模式呢?

内置类型工具

简单举几个类型工具的例子:

增加字段

type X = {
  a: number;
  b: number;
};

type Y = X & {
  c: number;
};

const x: X = { a: 1, b: 2 };
const y: Y = { a: 1, b: 2, c: 3 };

合并

type X = {
  a: number;
  b: number;
};

type Y = {
  c: number;
};

type Z = X & Y;

const x: X = { a: 1, b: 2 };
const y: Y = { c: 3 };
const z: Z = { a: 1, b: 2, c: 3 };

实际上与1等价,因为1中的{ c: number; } 等价于就地定义了一个新的类型。

选取

type X = {
  a: number;
  b: number;
  c: number;
  d: number;
};

type Y = Pick<X, "b" | "c">;

const y: Y = { b: 1, c: 3 };

删除

type X = {
  a: number;
  b: number;
  c: number;
};

type Y = Omit<X, "b">;

const y: Y = { a: 1, c: 3 };

以上都是十分基础的用法,不涉及到任何复杂的类型体操,但除了1之外没有一个是能轻易在 Java 中实现的,必须通过反射之类的手段。对于更复杂的类型约束,Java 则根本无能为力了。

深入学习方法

类型学习

首先阅读文档,里面的内容非常丰富,可读性还是非常不错的。

其次,看https://github.com/gikey/tool-types,虽然它没有star,但更容易理解。

/**
 * Intersect
 * @desc 获取两个类型中属性的交集
 * @example
 *   type A = { name: string; age: number };
 *   type B = { name: string; address: string; gender: number; }
 *   // Expect: {name: string}
 *   Intersect<A, B>;
 */
export type Intersect<T, U> = Pick<T, Extract<keyof T, keyof U>>;

/**
 * Except
 * @desc 获取 A - B 差集
 * @example
 *   type A = { name: string; age: number };
 *   type B = { name: string; address: string; gender: number; }
 *   // Expect: { age: number; }
 *   Except<A, B>;
 */
export type Except<T, U> = Pick<T, Exclude<keyof T, keyof U>>;

export type UnionOmit<T, U> = T & Omit<T, keyof U>;

export type TupleUnion<T> = T extends Array<infer U> ? U : never;

掌握了这些内容之后,基本类型就过关了。

然后看https://github.com/sindresorhus/type-fest,比如它的basic

export type Class<T, Arguments extends unknown[] = any[]> = {
 prototype: T;
 new(...arguments_: Arguments): T;
};

export type Constructor<T, Arguments extends unknown[] = any[]> = new(...arguments_: Arguments) => T;

export interface AbstractClass<T, Arguments extends unknown[] = any[]> extends AbstractConstructor<T, Arguments> {
 prototype: T;
}

export type AbstractConstructor<T, Arguments extends unknown[] = any[]> = abstract new(...arguments_: Arguments) => T;

export type JsonObject = {[Key in string]: JsonValue} & {[Key in string]?: JsonValue | undefined};

export type JsonArray = JsonValue[] | readonly JsonValue[];

export type JsonPrimitive = string | number | boolean | null;

export type JsonValue = JsonPrimitive | JsonObject | JsonArray;

再去看其他的就简单多了,比如jsonify。

export type Jsonify<T> = IsAny<T> extends true
 ? any
 : T extends PositiveInfinity | NegativeInfinity
  ? null
  : T extends JsonPrimitive
   ? T
   : // Instanced primitives are objects
   T extends Number
    ? number
    : T extends String
     ? string
     : T extends Boolean
      ? boolean
      : T extends Map<any, any> | Set<any>
       ? EmptyObject
       : T extends TypedArray
        ? Record<string, number>
        : T extends NotJsonable
         ? never // Non-JSONable type union was found not empty
         : // Any object with toJSON is special case
         T extends {toJSON(): infer J}
          ? (() => J) extends () => JsonValue // Is J assignable to JsonValue?
           ? J // Then T is Jsonable and its Jsonable value is J
           : Jsonify<J> // Maybe if we look a level deeper we'll find a JsonValue
          : T extends []
           ? []
           : T extends unknown[]
            ? JsonifyList<T>
            : T extends readonly unknown[]
             ? JsonifyList<WritableDeep<T>>
             : T extends object
              ? JsonifyObject<UndefinedToOptional<T>> // JsonifyObject recursive call for its children
              : never; // Otherwise any other non-object is removed