TypeScript基础知识

94 阅读20分钟

一、日常类型

1、函数

返回Promise的函数

async function getFavoriteNumber(): Promise<number> {
  return 26;
}

2、字面推断

function handleRequest(url: string, method: "GET" | "POST"): void;
function handleRequest(url: string, method: "GET" | "POST") {
  console.log(url, method);
}

const req = { url: "https://example.com", method: "GET" };

// 报错:Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.
handleRequest(req.url, req.method); 
  
// 解决方式一
handleRequest(req.url, req.method as "GET");
  
// 解决方式二
const req = { url: "https://example.com", method: "GET" as "GET" };
  
// 解决方案三
const req = { url: "https://example.com", method: "GET" } as const

3、非空断言运算符(后缀 !)

在不进行任何显式检查的情况下从类型中删除null和undefined。在任何表达式之后写!实际上是一个类型断言,该值不是null或undefined

function liveDangerously(x?: number | null) {
  console.log(x!.toFixed());
}

二、更多关于函数

1、调用签名

type DescribableFunction = {
  description: string;
  (someArg: number): boolean;
};
function doSomething(fn: DescribableFunction) {
  console.log(fn.description + " returned " + fn(6));
}

function myFunc(someArg: number) {
  return someArg > 3;
}
myFunc.description = "default description";

doSomething(myFunc);

2、构造签名

type SomeObject = {
  name: string;
};

type SomeConstructor = {
  // 构造签名
  new (s: string): SomeObject;
};

function fn(ctor: SomeConstructor) {
  return new ctor("jack");
}

interface CallOrConstruct {
  // 构造签名和非构造签名
  new (s: string): Date;
  (n?: number): string;
}

3、在函数中声明this

interface User {
  admin: boolean;
}

interface DB {
  filterUsers(filter: (this: User) => boolean): User[];
}

const db = getDB();
const admins = db.filterUsers(function (this: User) {
  return this.admin;
});

// 报错:'this' implicitly has type 'any' because it does not have a type annotation
const admins = db.filterUsers(() => this.admin);

4、数据类型

object:任何非原始值(string、number、bigint、boolean、symbol、null或undefined)。与全局类型Object不同。函数类型在TypeScript中被认为是object

unknown:代表任何值。 使用unknown值做任何事情都是不合法的

never:表示从未观察到的值。 在返回类型中,这意味着函数抛出异常或终止程序的执行

Function:描述了bind、call、apply等属性,以及JavaScript中所有函数值上的其他属性。如果你需要接受任意函数但不打算调用它,则类型 () => void 通常更安全

三、对象类型

1、readonly属性

通过别名来更改readonly属性

interface Person {
  name: string;
  age: number;
}

interface ReadonlyPerson {
  readonly name: string;
  readonly age: number;
}

let writablePerson: Person = {
  name: "Person McPersonface",
  age: 42,
};

let readonlyPerson: ReadonlyPerson = writablePerson;
console.log(readonlyPerson.age); // prints '42'
writablePerson.age++;
console.log(readonlyPerson.age); // prints '43'

2、溢出属性检查

interface SquareConfig {
  color?: string;
  width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
  return {
    color: config.color || "red",
    area: config.width ? config.width * config.width : 20,
  };
}

// 报错:Argument of type '{ colour: string; width: number; }' is not assignable to parameter of type 'SquareConfig'. Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?
let mySquare = createSquare({ colour: "red", width: 100 });

// 解决方式一
let mySquare = createSquare({ colour: "red", width: 100 } as SquareConfig);

// 解决方式二
interface SquareConfig {
  color?: string;
  width?: number;
  [propName: string]: any;
}

// 解决方式三:squareOptions和SquareConfig要有共同属性(width)
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);

四、keyof 类型运算符

1、对象类型使用keyof操作符,会返回该对象属性名组成的一个字符串或者数字字面量的联合

// 字符串字面量的联合
type Point = { x: number; y: number };
// 等同于type mPoint = "x" | "y"
type mPoint = keyof Point;

// 数字字面量的联合
const NumericObject = {
  1: "java",
  2: "C#",
  3: "Python",
};

// typeof NumericObject等同于{  
//   1: string;  
//   2: string;  
//   3: string;  
// }

// keyof typeof NumericObject等同于1 | 2 | 3

// 等同于type result = 1 | 2 | 3
type result = keyof typeof NumericObject;

2、如果对象类型有一个string或者number类型的索引签名,keyof则会直接返回这些类型

type Arrayish = { [n: number]: unknown };
// 等同于type A = number
type A = keyof Arrayish;

type Mapish = { [k: string]: boolean };
// 等同于type M = string | number
type M = keyof Mapish;

3、对类使用keyof

class Arrayish {
  name: "TS";
}
// 等同于type result = "name"
type result = keyof Arrayish;

class Mapish {
  [1]: string = "TS";
}
// 等同于type result = 1
type result = keyof Mapish;

4、对接口使用keyof

interface Arrayish {
  name: "string";
}
// 等同于type result = "name"
type result = keyof Arrayish;

5、对Symbol使用keyof

const sym1 = Symbol();
const sym2 = Symbol();
const sym3 = Symbol();

const symbolToNumberMap = {
  [sym1]: 1,
  [sym2]: 2,
  [sym3]: 3,
};

// 等同于type ks = unique symbol | unique symbol | unique symbol
type ks = keyof typeof symbolToNumberMap;

五、typeof 类型运算符

1、对象使用typeof

const person = { name: "kevin", age: "18" };

// 等同于type Kevin = {  
//   name: string;  
//   age: string;  
// }
type kevin = typeof person;

2、对函数使用typeof

function identity<Type>(arg: Type): Type {
  return arg;
}
  
// 等同于type result = <Type>(arg: Type) => Type
type result = typeof identity;

3、对enum使用typeof

enum UserResponse {
  No = 0,
  Yes = 1,
}

// 等同于type result = {
//   No: number;
//   YES: number;
// };
type result = typeof UserResponse;

4、对Symbol使用typeof

const sym1 = Symbol();
const sym2 = Symbol();
const sym3 = Symbol();

const symbolToNumberMap = {
  [sym1]: 1,
  [sym2]: 2,
  [sym3]: 3,
};

// 等同于type ks = {  
//   [sym1]: number;  
//   [sym2]: number;  
//   [sym3]: number;  
// }
type ks = typeof symbolToNumberMap;

5、对数组元素是对象使用typeof

const MyArray = [
  { name: "Alice", age: 15 },
  { name: "Bob", age: 23 },
  { name: "Eve", age: 38 },
];

// 等同于type Person = {  
//   name: string;  
//   age: number;  
// }[]
type Person = typeof MyArray;

6、对元组元素是对象使用typeof

const MyArray = [
  { name: "Alice", age: 15 },
  { name: "Bob", age: 23 },
  { name: "Eve", age: 38 },
] as const;

// 等同于type Person = readonly [
//   {
//     readonly name: "Alice";
//     readonly age: 15;
//   },
//   {
//     readonly name: "Bob";
//     readonly age: 23;
//   },
//   {
//     readonly name: "Eve";
//     readonly age: 38;
//   }];
  
type Person = typeof MyArray;

注意:数组和元组对象使用typeof返回的值完全不一样

六、索引访问类型

使用索引访问类型来查找另一种类型的特定属性

type Person = { age: number; name: string; alive: boolean };

// 等同于type age = number
type age = Person["age"];

// 等同于type i1 = string | number
type i1 = Person["age" | "name"];

// 等同于type i2 = string | number | boolean
type i2 = Person[keyof Person];

type AliveOrName = "alive" | "name";
// 等同于type i3 = string | boolean
type i3 = Person[AliveOrName];


const MyArray = [
  { name: "Alice", age: 15 },
  { name: "Bob", age: 23 },
  { name: "Eve", age: 38 },
];

// 等同于type Person = {  
//   name: string;  
//   age: number;  
// }
type Person = (typeof MyArray)[number];

// 等同于type Age = number
type Age = (typeof MyArray)[number]["age"];

七、条件类型

条件类型是描述输入类型和输出类型之间的关系(基于输入的值的类型来决定输出的值的类型)

1、条件类型

条件类型的写法有点类似于:SomeType extends OtherType ? TrueType : FalseType

interface Animal {
  live(): void;
}

interface Dog extends Animal {
  woo(): void;
}

// 等同于type example1 = number
type example1 = Dog extends Animal ? number : string;

// 等同于type example2 = string
type example2 = RegExp extends Animal ? number : string;


interface IdLabel {
  id: number;
}
interface NameLabel {
  name: string;
}

function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
  throw "unimplemented";
}
//上面的函数重载是基于输入的值的类型(参数的类型)来决定输出的值的类型(返回值的类型)

//采用条件类型实现
type NameOrId<T extends string | number> = T extends string
  ? NameLabel
  : IdLabel;
  
function createLabe2<T extends string | number>(nameOrId: T): NameOrId<T> {
  throw "unimplemented";
}

2、条件类型约束

type MessageOf<T> = T extends { message: any } ? T["message"] : never;

interface Email {
  message: string;
}

interface Dog {
  bark(): void;
}

// 等同于type EmailMessageContents = string
type EmailMessageContents = MessageOf<Email>;

// 等同于type DogMessageContents = never
type DogMessageContents = MessageOf<Dog>;


type Flatten<T> = T extends any[] ? T[number] : T;

// 等同于type str = string
type str = Flatten<string[]>;

// 等同于type num = number
type num = Flatten<number>;

3、在条件类型里推断(infer(推断出)关键字)

type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;


type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
  ? Return
  : never;

// 等同于type Num = number
type Num = GetReturnType<() => number>;

// 等同于type Str = string
type Str = GetReturnType<(x: string) => string>;

// 等同于type Bools = boolean[]
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;

4、分发条件类型

type ToArray<Type> = Type extends any ? Type[] : never;
// 等同于type StrArrOrNumArr = string[] | number[]
type StrArrOrNumArr = ToArray<string | number>;

type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
// 等同于type StrArrOrNumArr2 = (string | number)[]
type StrArrOrNumArr2 = ToArrayNonDist<string | number>;

八、映射类型

有时一个类型需要基于另外一个类型,但是又不想拷贝一份,这个时候可以考虑使用映射类型

1、

type FeatureFlags = {
  darkMode: () => void;
  newUserProfile: () => void;
};

type OptionsFlags<Type> = {
  [Property in keyof Type]: boolean;
};

// 等同于type FeatureOptions = {  
//   darkMode: boolean;  
//   newUserProfile: boolean;  
// }
type FeatureOptions = OptionsFlags<FeatureFlags>;

2、映射修饰符

两个额外的修饰符,设置属性只读(readonly) 设置属性可选(?)。通过前缀-(删除)或者+(添加)处理修饰符,如果没有写前缀,相当于使用了+前缀

// 删除属性中的只读属性
type LockedAccount = {
  readonly id: string;
  readonly name: string;
};

type CreateMutable<T> = {
  -readonly [Property in keyof T]: T[Property];
};

// 等同于type unLockedAccount = {  
//   id: string;  
//   name: string;  
// }
type unLockedAccount = CreateMutable<LockedAccount>;

// 删除属性中的可选属性
type MaybeUser = {
  id: string;
  name?: string;
  age?: number;
};

type Concrete<Type> = {
  [Property in keyof Type]-?: Type[Property];
};

// 等同于type User = {  
//   id: string;  
//   name: string;  
//   age: number;  
// }
type User = Concrete<MaybeUser>;

3、通过as实现键名重新映射

interface Person {
  name: string;
  age: number;
  location: string;
}

type Getters<T> = {
  [Property in keyof T as `get${Capitalize<string & Property>}`]: () => T[Property];
};

// 等同于type LazyPerson = {  
//   getName: () => string;  
//   getAge: () => number;  
//   getLocation: () => string;  
// }
type LazyPerson = Getters<Person>;
interface Circle {
  kind: "circle";
  radius: number;
}

type RemoveKindField<T> = {
  [Property in keyof T as Exclude<Property, "kind">]: T[Property];
};

// 等同于type KindlessCircle = {  
//   radius: number;  
// }
type KindlessCircle = RemoveKindField<Circle>;
type SquareEvent = {
  kind: "square";
  x: number;
  y: number;
};

type CircleEvent = {
  kind: "circle";
  radius: number;
};

// Events是一个联合类型,Property in Events是循环联合类型,Property = SquareEvent和Property = CircleEvent
//先计算in运算符,再计算as计算符,Events可以类比是[SquareEvent,CircleEvent]
type EventConfig<Events extends { kind: string }> = {
  [Property in Events as Property["kind"]]: (event: Property) => void;
};

// 等同于type Config = {  
//   square: (event: SquareEvent) => void;  
//   circle: (event: CircleEvent) => void;  
// }
type Config = EventConfig<SquareEvent | CircleEvent>;

4、进一步探索

// 映射类型和条件类型结合
type DBFields = {
  id: { format: "incrementing" };
  name: { type: string; pii: true };
};

type ExtractPII<Type> = {
  [Property in keyof Type]: Type[Property] extends { pii: true }
    ? true
    : false;
};

// 等同于type ObjectsNeedingGDPRDeletion = {  
//   id: false;  
//   name: true;  
// }
type ObjectsNeedingGDPRDeletion = ExtractPII<DBFields>;

九、模板字面类型

模板字面类型以字符串字面类型为基础,可以通过联合类型扩展成多个字符串

1、模板字面量类型

type World = "world";

// 等同于type Greeting = "hello world"
type Greeting = `hello ${World}`;

type EmailLocaleIDs = "welcome_email" | "email_heading";

type FooterLocaleIDs = "footer_title" | "footer_sendoff";

// 等同于type AllLocaleIDs = "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;

type Lang = "en" | "ja" | "pt";

// 等同于type LocaleMessageIDs = "en_welcome_email_id" | "en_email_heading_id" | "en_footer_title_id" | "en_footer_sendoff_id" | "ja_welcome_email_id" | "ja_email_heading_id" | "ja_footer_title_id" | "ja_footer_sendoff_id" | "pt_welcome_email_id" | "pt_email_heading_id" | "pt_footer_title_id" | "pt_footer_sendoff_id"
type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`;

2、内置字符操作类型

// Uppercase:把每个字符转为大写形式

type Greeting = "Hello, world";

// 等同于type ShoutyGreeting = "HELLO, WORLD"
type ShoutyGreeting = Uppercase<Greeting>;

type ASCIICacheKey<Str extends string> = `ID-${Uppercase<Str>}`;

// 等同于type MainID = "ID-MY_APP"
type MainID = ASCIICacheKey<"my_app">;
// Lowercase:把每个字符转为小写形式

type Greeting = "Hello, world";
// 等同于type QuietGreeting = "hello, world"
type QuietGreeting = Lowercase<Greeting>;

type ASCIICacheKey<Str extends string> = `id-${Lowercase<Str>}`;
// 等同于type MainID = "id-my_app"
type MainID = ASCIICacheKey<"MY_APP">;
// Capitalize:把字符串的第一个字符转为大写形式

type LowercaseGreeting = "hello, world";
// 等同于type Greeting = "Hello, world"
type Greeting = Capitalize<LowercaseGreeting>;
// Uncapitalize:把字符串的第一个字符转换为小写形式

type UppercaseGreeting = "HELLO WORLD";
// 等同于type UncomfortableGreeting = "hELLO WORLD"
type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>;

十、类

1、类成员

字段

如果执意要通过其他方式初始化一个字段,而不是在构造函数里(举个例子,引入外部库为你补充类的部分内容),你可以使用明确赋值断言操作符!

class OKGreeter {
  name!: string;
}
Super调用

如果你有一个基类,你需要在使用任何this.成员之前,先在构造函数里调用super()

class Base {
  k = 4;
}

class Derived extends Base {
  constructor() {
    super();
    console.log(this.k);
  }
}
Getters/Setter

从TypeScript4.3起,存取器在读取和设置的时候可以使用不同的类型

class Thing {
  _size = 0;

  get size(): number {
    return this._size;
  }

  // 注意这里允许传入的是 string | number | boolean 类型
  set size(value: string | number | boolean) {
    let num = Number(value);

    if (!Number.isFinite(num)) {
      this._size = 0;
      return;
    }

    this._size = num;
  }
}
索引签名

类可以声明索引签名,它和对象类型的索引签名是一样的

class MyClass {
  [s: string]: boolean | ((s: string) => boolean);

  check(s: string) {
    return this[s] as boolean;
  }
  
  change(s: string) {
    const fun = this[s] as (s: string) => boolean;
    return fun(s);
  }
}

2、类继承

implements语句

implements语句仅仅检查类是否按照接口类型实现,但它并不会改变类的类型或者方法的类型。implements语句并不会影响类的内部是如何检查或者类型推断的

interface Checkable {
  check(name: string): boolean;
}

class NameChecker implements Checkable {
  check(s: any) {
    return s.toLowercse() === "ok";
  }
}

实现一个有可选属性的接口,并不会创建这个属性

interface A {
  x: number;
  y?: number;
}
class C implements A {
  x = 0;
}
const c = new C();
//Property 'y' does not exist on type 'C'(属性y在类型C上不存在)
c.y = 10;
extends 语句

覆写属性

class Base {
  greet() {
    console.log("Hello, world!");
  }
}

class Derived extends Base {
  greet(name?: string) {
    if (name === undefined) {
      console.log("***super***");
      super.greet();
    } else {
      console.log(`Hello, ${name.toUpperCase()}`);
    }
  }
}

const d = new Derived();
d.greet();
d.greet("reader");

console.log("******");
const b: Base = d;
b.greet();

初始化顺序

//1.基类字段初始化
//2.基类构造函数运行
//3.派生类字段初始化
//4.派生类构造函数运行
class Base {
  name = "base";
  constructor() {
    console.log("My name is " + this.name);
  }
}

class Derived extends Base {
  name = "derived";
}

const d = new Derived(); //输出My name is base

3、成员可见性

public:默认的可见性修饰符

protected:仅仅对子类可调用,子类重写可以让一个protected成员变成public

class Base {
  protected m = 10;

  protected getName() {
    return "hi";
  }
}
  
class Derived extends Base {
  m = 15;

  public howdy() {
    //子类可以调用protected的方法
    console.log("Howdy, " + this.getName());
  }
}

const d = new Derived();
//重写把protected改成public
console.log(d.m);

d.howdy();
//报错:Property 'getName' is protected and only accessible within class 'Base' and its subclasses.
//子类外部不能调用protected的方法
d.getName();

private:只能在本身的类中被访问

4、静态成员

类可以有静态成员,静态成员跟类实例没有关系,可以通过类本身访问到

静态成员同样可以使用public protected和private这些可见性修饰符

静态成员也可以被继承

class Base {
  static getGreeting() {
    return "Hello world";
  }
}

class Derived extends Base {
  private static x = 0;
  static y = "hello";
  myGreeting = Derived.getGreeting();
}

console.log(Derived.y);

注:TypeScript和JavaScript并没有名为静态类

5、类静态块

静态块用于写初始化代码,可以获取类里的私有字段。还可以完全获取类中的属性和方法

class Foo {
  static count = 0;

  get count() {
    return Foo.count;
  }

  static {
    Foo.count += 1;
  }
}

6、类运行时的this

TypeScript不会改变JavaScript的运行时行为,TypeScript提供了一些减轻或防止此类错误的方法

class MyClass {
  name = "MyClass";

  getName(this: MyClass) {
    return this.name;
  }
}

const c = new MyClass();
const g = c.getName;
  
// The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.
// 类型为` void `的` this `上下文不能赋值给类型为` MyClass `的` this `方法
console.log(g());

7、参数属性

在构造函数参数前添加一个可见性修饰符public private protected或者readonly来创建参数属性,这些构造函数参数转成一个同名同值的类属性

class Params {
  // 等同于定义类属性
  // readonly x: number;
  // protected y: number;
  // private z: number;

  constructor(
    public readonly x: number,
    protected y: number,
    private z: number
  ) {}
}

8、abstract类和成员

类、方法和字段可能是抽象的

抽象方法或抽象字段是尚未提供实现的方法。这些成员必须存在于抽象类中,不能直接实例化

抽象类的作用是作为实现所有抽象成员的子类的基类。当一个类没有任何抽象成员时,那么它是具体的

抽象类中可以没有抽象方法和抽象字段

abstract class Base {
  abstract getName(): string;

  printName() {
    console.log("Hello, ");
  }
}

class Derived extends Base {
  getName(): string {
    return "world";
  }
}

const d = new Derived();
d.printName();

9、类之间的关系

结构相同的两个类可以替代

class Point1 {
  x = 0;
  y = 0;
}

class Point2 {
  x = 0;
  y = 0;
}

const p: Point1 = new Point2();

即使没有显式继承,类之间的子类型关系也存在

class Person {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

class Employee {
  name: string;
  age: number;
  salary: number;
  constructor(name: string, age: number, salary: number) {
    this.name = name;
    this.age = age;
    this.salary = salary;
  }
}

const p: Person = new Employee("jack", 21, 3500);

十一、模块

1、模块与脚本

任何包含顶层import或export的文件都被视为模块,模块在它们自己的作用域内执行,而不是在全局作用域内。这意味着在模块中声明的变量、函数、类等在模块外部是不可见的,除非它们使用一种导出形式显式导出

没有任何顶层import或export声明的文件被视为脚本,在脚本文件中,变量和类型被声明在共享全局作用域内

2、TypeScript 中的模块

ES 模块语法

声明类型导入(import type)

// @filename: animal.ts
export type Cat = { breed: string; yearOfBirth: number };
export type Dog = { breeds: string[]; yearOfBirth: number };
export const createCatName = () => "fluffy";

// @filename: valid.ts
// import type导入声明类型
import type { Cat, Dog } from "./animal.js";
export type Animals = Cat | Dog;

// @filename: app.ts
// 把类型和其他一起导入
import { createCatName, type Cat, type Dog } from "./animal.js";
 
export type Animals = Cat | Dog;
const name = createCatName();

十二、工具类型

TypeScript提供了几种工具类型来促进常见的类型转换,把一个类型转换为另外一种类型

Awaited<Type>:递归地解开Promise后组成的类型
Partial<Type>:构造一个将Type的所有属性设置为可选的类型
Required<Type>:构造一个由设置为required的Type的所有属性组成的类型
Readonly<Type>:构造一个将Type的所有属性设置为readonly的类型
....
ReturnType<Type>:构造一个由函数Type的返回类型组成的类型
InstanceType<Type>:构造一个由Type中的构造函数的实例类型组成的类型

十三、声明合并

1、合并接口

接口合并将两个声明的成员机械地连接到一个同名的接口中

接口的非函数成员应该是唯一的。如果它们不是唯一的,则它们必须属于同一类型。如果接口都声明了同名但类型不同的非函数成员,编译器将触发错误

interface Box {
  height: number;
  width: number;
}

interface Box {
  scale: number;
  // 非函数成员是相同类型
  width: number;
  // 非函数成员不是相同类型,报错
  // height: string;
}

//三个接口合并后的声明
interface Box {
  height: number;
  width: number;
  scale: number;
}

let box: Box = { height: 5, width: 6, scale: 10 };

对于函数成员,每个同名的函数成员都被视为描述同一函数的重载。同样值得注意的是,后面接口比前面接口有更高的优先级,每个接口的函数成员都保持相同的顺序

interface Animal {}

interface Sheep {}

interface Dog {}

interface Cat {}

interface Cloner {
  clone(animal: Animal): Animal;
}
  
interface Cloner {
  clone(animal: Sheep): Sheep;
}
  
interface Cloner {
  clone(animal: Dog): Dog;
  clone(animal: Cat): Cat;
}

//三个接口合并后的声明
interface Cloner {
  clone(animal: Dog): Dog;
  clone(animal: Cat): Cat;
  clone(animal: Sheep): Sheep;
  clone(animal: Animal): Animal;
}

如果签名具有类型为单个字符串字面类型的参数(例如,不是字符串字面的联合),那么它将冒泡到其合并重载列表的顶部

interface Document {
  createElement(tagName: any): Element;
}

interface Document {
  createElement(tagName: "div"): HTMLDivElement;
  createElement(tagName: "span"): HTMLSpanElement;
}

interface Document {
  createElement(tagName: string): HTMLElement;
  createElement(tagName: "canvas"): HTMLCanvasElement;
}

//三个接口合并后的声明
interface Document {
  createElement(tagName: "canvas"): HTMLCanvasElement;
  createElement(tagName: "div"): HTMLDivElement;
  createElement(tagName: "span"): HTMLSpanElement;
  createElement(tagName: string): HTMLElement;
  createElement(tagName: any): Element;
}

2、合并命名空间

来自在每个命名空间中声明的导出接口的类型定义本身被合并,形成一个单一的命名空间,其中包含合并的接口定义

namespace Animals {
  export class Zebra {}
}

namespace Animals {
  export interface Legged {
    numberOfLegs: number;
  }
  export class Dog {}
}

//合并命名空间合并后的声明
namespace Animals {
  export interface Legged {
    numberOfLegs: number;
  }
  export class Zebra {}
  export class Dog {}
}

非导出成员仅在原始(未合并)命名空间中可见。这意味着合并后,来自其他声明的合并成员看不到非导出成员

namespace Animal {
  let haveMuscles = true;
  export function animalsHaveMuscles() {
    return haveMuscles;
  }
}
  
namespace Animal {
  export function doAnimalsHaveMuscles() {
    return haveMuscles;
  }
}

因为haveMuscles没有导出,所以只有共享同一个未合并命名空间的animalsHaveMuscles函数才能看到该变量。
doAnimalsHaveMuscles函数,即使它是合并的Animal命名空间的一部分,也无法看到这个未导出的成员

注:类不能与其他类或变量合并

十四、Symbol

1、unique symbol

unique symbol是symbol的子类型,仅通过调用Symbol()或Symbol.for()或显式类型注释产生。这种类型只允许在const声明和readonly static属性上使用

const sym1: unique symbol = Symbol();

class C {
  static readonly StaticSymbol: unique symbol = Symbol();
}

十五、类型兼容性

如果y至少具有与x相同的成员,则x与y兼容

interface Pet {
  name: string;
}

let pet: Pet;
let dog = { name: "Lassie", owner: "Rudd Weatherwax" };

// dog是否与pet兼容,编译器会检查pet的每个属性在dog中找到对应的兼容属性。所以,dog必须有一个name的成员
pet = dog;
function greet(pet: Pet) {
  console.log("Hello, " + pet.name);
}
greet(dog);

// Error:Object literal may only specify known properties, and 'owner' does not exist in type 'Pet'
let dog1: Pet = { name: "Lassie", owner: "Rudd Weatherwax" };

1、比较两个函数

函数参数的名字相同与否并无所谓,只关注类型
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;

// x的每个参数在y中都能找到对应的参数,所以允许赋值
y = x; // OK
// y有个必需的第二个参数,但是x并没有,所以不允许赋值
x = y; // Error


源函数的返回类型是目标类型的返回类型的子类型
let x = () => ({ name: "Alice" });
let y = () => ({ name: "Alice", location: "Seattle" });
x = y; // OK
y = x; // Error

当源函数参数能够赋值给目标函数或者反过来时才能赋值成功。 这是不安全的,因为调用者可能传入了一个具有更精确类型信息的函数,但是调用这个传入的函数的时候却使用了不是那么精确的类型信息。 实际上,这极少会发生错误,允许这种模式是为了兼容JavaScript中许多常见模式

type EventType = MyMouseEvent | MyKeyEvent;

interface TestEvent {
  timestamp: number;
}
  
interface MyMouseEvent extends TestEvent {
  x: number;
  y: number;
}
  
interface MyKeyEvent extends TestEvent {
  keyCode: number;
}
  
function listenEvent(event: EventType, handler: (n: TestEvent) => void): void {
  handler(event);
}
  
const mouseEvent: MyMouseEvent = {
  timestamp: 123,
  x: 123,
  y: 456,
};
  
const keyEvent: MyKeyEvent = {
  timestamp: 456,
  keyCode: 789,
};
  
// 不安全但好用又常见
listenEvent(mouseEvent, (e: MyMouseEvent) => console.log(e.x + "," + e.y));

// 不安全的原因:可能会传入一个不是MyMouseEvent但是又是Event的类型到listenEvent中,可能导致该方法错误
listenEvent(keyEvent, (e: MyMouseEvent) => console.log(e.x + "," + e.y)); // Error!

// 安全但不尽如人意的方案
listenEvent(mouseEvent, (e: TestEvent) =>
  console.log((e as MyMouseEvent).x + "," + (e as MyMouseEvent).y)
);

listenEvent(mouseEvent, ((e: MyMouseEvent) => console.log(e.x + "," + e.y)) as (e: TestEvent) => void);

可选参数及剩余参数在比较函数兼容性的时候,可选参数与必须参数是可互换的。 源类型上有额外的可选参数不会产生错误,目标类型的可选参数在源类型里没有对应的参数也不会产生错误。 当一个函数有剩余参数时,它被当做无限个可选参数

2、枚举

枚举与数字兼容,数字与枚举兼容,不同枚举类型的枚举值被认为是不兼容的

enum Status {
  ady,
  Waiting,
}
  
enum Color {
  Red,
  Blue,
  Green,
}
  
let status = Status.ady;
status = Color.Red; // Error

3、类

当比较一个类类型的两个对象时,只比较实例的成员。静态成员和构造函数不影响兼容性

class Animal {
  feet: number;
  constructor(name: string, numFeet: number) {
    this.feet = numFeet;
  }
}

class Size {
  feet: number;
  constructor(numFeet: number) {
    this.feet = numFeet;
  }
}

let a: Animal;
let s: Size;

a = s; // OK
s = a; // OK

类的私有成员和受保护成员

类的私有成员和受保护成员会影响兼容性。 当检查类实例的兼容时,如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员。 同样地,这条规则也适用于包含受保护成员实例的类型检查

3、泛型

interface Empty<T> {}
let x: Empty<number>;
let y: Empty<string>;
// 没有成员时,它们的结构使用类型参数时并没有什么不同,所以x和y是兼容的
x = y; // OK


interface NotEmpty<T> {
    data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;
// 有成员时,x和y是不兼容的
x = y; // Error

十六、类型推断

1、上下文类型

如果此函数不在上下文类型位置,则函数的参数将隐式具有类型any,并且不会触发错误(除非你使用noImplicitAny选项)

// uiEvent默认就是any类型
const handler = function (uiEvent) {
  console.log(uiEvent.button); // OK
};

十七、声明文件

1、具有属性的对象

//文档
全局变量myLib有一个函数makeGreeting用于创建问候语,还有一个属性numberOfGreetings表示到目前为止问候语的数量

//代码
let result = myLib.makeGreeting("hello, world");
console.log("The computed greeting is:" + result);
let count = myLib.numberOfGreetings;

//声明文件
declare namespace myLib {
  function makeGreeting(s: string): string;
  let numberOfGreetings: number;
}

2、重载函数

//文档
getWidget 函数接受一个数字并返回一个小部件,或者接受一个字符串并返回一个小部件数组

//代码
let x: Widget = getWidget(43);
let arr: Widget[] = getWidget("all of them");

//声明文件
declare function getWidget(n: number): Widget;
declare function getWidget(s: string): Widget[];

3、接口

//文档
指定问候语时,必须传递GreetingSettings对象。该对象具有以下属性:
问候语(greeting):必填字符串
期间(duration):可选的时间长度(以毫秒为单位)
颜色(color):可选字符串,例如'#ff00ff'

//代码
greet({
  greeting: "hello world",
  duration: 4000
});

//声明文件
interface GreetingSettings {
  greeting: string;
  duration?: number;
  color?: string;
}
declare function greet(setting: GreetingSettings): void;

4、类型别名

//文档
在任何需要问候语的地方,你都可以提供 string、返回 string 的函数或 Greeter 实例。

//代码
function getGreeting() {
  return "howdy";
}
class MyGreeter extends Greeter {}
greet("hello");
greet(getGreeting);
greet(new MyGreeter());

//声明文件
type GreetingLike = string | (() => string) | MyGreeter;
declare function greet(g: GreetingLike): void;

5、组织类型

//文档
greeter 对象可以记录到文件或显示警报。你可以向 .log(...) 提供 LogOptions,向 .alert(...) 提供警报选项

//代码
const g = new Greeter("Hello");
g.log({ verbose: true });
g.alert({ modal: false, title: "Current Greeting" });

//声明文件

//1.使用名称空间来组织类型
declare namespace GreetingLib {
  interface LogOptions {
    verbose?: boolean;
  }
  
  interface AlertOptions {
    modal: boolean;
    title?: string;
    color?: string;
  }
}

//2.一个声明中创建嵌套的命名空间
declare namespace GreetingLib.Options {   
  interface Log {
    verbose?: boolean;
  }
  
  interface Alert {
    modal: boolean;
    title?: string;
    color?: string;
  }
}

6、类

//文档
你可以通过实例化 Greeter 对象来创建欢迎程序,或者通过从它扩展来创建自定义的欢迎程序

//代码
const myGreeter = new Greeter("hello, world");
myGreeter.greeting = "howdy";
myGreeter.showGreeting();
class SpecialGreeter extends Greeter {
  constructor() {
    super("Very special greetings");
  }
}

//声明文件
declare class Greeter {
  constructor(greeting: string);
  greeting: string;
  showGreeting(): void;
}

7、全局变量

//文档
全局变量 foo 包含存在的小部件数量

//代码
console.log("Half the number of widgets is " + foo / 2);

//声明文件
//使用declare var声明变量。如果变量是只读的,使用declare const。如果变量是块作用域的,使用declare let

declare var foo: number;

8、全局函数

//文档
你可以使用字符串调用函数 greet 以向用户显示问候语

//代码
greet("hello, world");

//声明文件
declare function greet(greeting: string): void;

9、该做什么和不该做什么

不要使用类型Number、String、Boolean、Symbol或Object这些非原始装箱对象,请使用number、string、boolean、symbol或object 

不要将返回类型any用于其值将被忽略的回调,对于其值将被忽略的回调,请使用返回类型void,因为它可以防止你意外地以未经检查的方式使用 x 的返回值

//WRONG
function fn(x: () => any) {
  x();
}

//OK
function fn(x: () => void) {
  x();
}

//解释:返回void就无法使用x的返回值,如果是any可以使用x的返回值
function fn(x: () => void) {
  var k = x(); 
  k.doSomething(); // error, but would be OK if the return type had been 'any'
}

不要在回调中使用可选参数,请将回调参数写为非可选

//WRONG
interface Fetcher {
  getObject(done: (data: unknown, elapsedTime?: number) => void): void;
}

//OK
interface Fetcher {
  getObject(done: (data: unknown, elapsedTime: number) => void): void;
}

不要编写仅在回调数量上不同的单独重载,请使用最大数量编写单个重载

//WRONG
declare function beforeAll(action: () => void, timeout?: number): void;
declare function beforeAll(
  action: (done: DoneFn) => void,
  timeout?: number
): void;

//OK
declare function beforeAll(
  action: (done: DoneFn) => void,
  timeout?: number
): void;

不要将更通用的重载放在更具体的重载之前,请将更通用的签名放在更具体的签名后面来对重载进行排序,因为TypeScript在解析函数调用时选择第一个匹配的重载。如果更通用的重载在前面会直接匹配上

// WRONG 
declare function fn(x: unknown): unknown;
declare function fn(x: HTMLElement): number;
declare function fn(x: HTMLDivElement): string;
var myElem: HTMLDivElement;
var x = fn(myElem); // x: unknown, wat?

// OK
declare function fn(x: HTMLDivElement): string;
declare function fn(x: HTMLElement): number;
declare function fn(x: unknown): unknown;
var myElem: HTMLDivElement;
var x = fn(myElem); // x: string, :)

不要编写仅尾随参数不同的多个重载(注意:只有当所有重载都具有相同的返回类型时),尽可能使用可选参数

//WRONG
interface Example {
  diff(one: string): number;
  diff(one: string, two: string): number;
  diff(one: string, two: string, three: boolean): number;
}

// OK
interface Example {
  diff(one: string, two?: string, three?: boolean): number;
}

不要仅在一个参数位置编写因类型而异的重载,尽可能使用联合类型

//WRONG
interface Moment {
  utcOffset(): number;
  utcOffset(b: number): Moment;
  utcOffset(b: string): Moment;
}

// OK
interface Moment {
  utcOffset(): number;
  utcOffset(b: number | string): Moment;
}

十八、参考资料

1.TypeScript挑战

2.TypeScript官网

3.TypeScript中文官方网站