跳至主要內容

TypeScript

ourandream大约 19 分钟front-end

在大型 js 应用中,Debug 因为 js 灵活的 type 变得非常困难.TypeScript相对于在 js 的基础上添加了类型检查系统,为大型应用的构建提供了便利,同时也为可以增强编辑器的代码补全功能. 本文是对 ts 官网 handbook 的内容的总结.

Everyday Types

js 中的常用的 primitives(String,Number,Boolean)在 ts 对应的是string, number, and boolean.大写开头的类型名也合法,但为了于一些内置的 type 兼容,最好使用小写开头. 对应较少使用的 primitives(BigInt,Symbol)对应的是bigintsymbol. 某种类型的数字的写法有string[]Array<number> js 中的 any 对应的是any,默认可以出现 any,不过如果我们设置了更严格的检查,我们需要注释any类型. 变量类型注释(type annotation):

let myName: string = "Alice";

通常不需要注释,ts 会自动推断出变量的类型. 函数参数注释:

function greet(name: string) {
  console.log("Hello, " + name.toUpperCase() + "!!");
}

返回值注释:

function getFavoriteNumber(): number {
  return 26;
}

通常返回值不需要注释,ts 会自动推断出来. 在有些场合(如 Anonymous Functions),ts 也可以把参数类型推断出来. 对象的类型(可以用;分隔也可以用,):

function printCoord(pt: { x: number; y: number }) {
	...
}

如果某个 property 没有注释,则它会是 any,若设置了严格检查,可能会报错. 设置 property 可选(?):

function printName(obj: { first: string; last?: string }) {

	// Error - might crash if 'obj.last' wasn't provided!

	console.log(obj.last.toUpperCase());

	Object is possibly 'undefined'.Object is possibly 'undefined'.

	if (obj.last !== undefined) {

	// OK

	console.log(obj.last.toUpperCase());

}

// A safe alternative using modern JavaScript syntax:

console.log(obj.last?.toUpperCase());

}

我们可以使用Union Types组合一系列 type:

function printId(id: number | string) {
  console.log("Your ID is: " + id);
}
// OK

printId(101);

// OK

printId("202");

// Error

printId({ myID: 22342 });

此时它仅可使用这些 type 共有的操作:

function printId(id: number | string) {
  //error
  console.log(id.toUpperCase());
}

可以用在 if 分支中检查类型解决:

function printId(id: number | string) {
  if (typeof id === "string") {
    // In this branch, id is of type 'string'
    //sometimes use Array.isArray to check

    console.log(id.toUpperCase());
  } else {
    // Here, id is of type 'number'

    console.log(id);
  }
}

注意在 else 中不需要检查,ts 已经自动推断出 type. 我们可以为type取别名:

type Point = {
  x: number;

  y: number;
};

注意仅仅是取别名,而不是弄了一个不同的版本.它在 ts 的表现和原来的 type 一样. type可以为 union,object type 或者 primitives 区别名,对应 object type,也可以使用interface:

interface Point {
  x: number;

  y: number;

  method1(): string;
}

它与type的不同点:

  • 在 TypeScript version 4.2 之前, type alias names 可能出现在 error messages. Interfaces 总是会 error messages.
  • interface 可以添加内容,而 type alias 不可以.
  • 只能重命名 object type.
  • Interface 仅在使用它的命名时会出现的 error message,即如果使用形式为{ name: string }的形式,即使已经使用 interface 命名它了,ts 的 error message 也并不会出现该名字. 我们可以进行type assertion来指定一个更具体的 type:
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;

注意 ts 只允许指定一个更具体的 type,故由 string 指定为 number 是错误的. 我们可以使用 string 或 number 来生成定义 type,称为literal type:

let x: "hello" = "hello";

// OK

x = "hello";

// error

x = "howdy";

和 union 联合起来使用:

function printText(s: string, alignment: "left" | "right" | "center") {
  // ...
}

可以结合其他的 type:

function configure(x: Options | "auto") {
  // ...
}

注意booleantrue | false的别名. 在 ts 中使用literal type有时会遇到如下情况:

const req = { url: "https://example.com", method: "GET" };
//error
handleRequest(req.url, req.method); //第二个参数为 "GET"|"POST"

可以通过type assertion解决:

// Change 1:

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

// Change 2

handleRequest(req.url, req.method as "GET");

也可以通过as const解决:

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

handleRequest(req.url, req.method);

as const使得所有 properties 都使用literal type. 对应 null 和 undefined,我们可以使用!说明不会是它们之一:

function liveDangerously(x?: number | null) {
  // No error

  console.log(x!.toFixed());
}

注意这不好改变编译出来的 js 代码,故还是需要做相关的对应 null 和 undefined 的处理. enum是在 js 中不存在,但 ts 添加的功能.只在必要时使用.

narrowing

ts 分析代码并更精确地确定 type 的功能较narrowing.它借助一些特殊结构的称为 type guard 的代码确定 type. 首先是 js 自带的typeof,这自然能用来确定 type.ts 同时还会注意 js 中的一些坑,如 typeof null 是 object. 然后是 truthiness,即 if 等条件状态语句自动将非 boolean 变量转化为条件的功能. 然后是等式和不等式.ts 还会注意!=等运算符的坑(如 null==undefined). 然后是检查 property 是否在内的 in 运算符. 然后是检查是否为某个构造函数的 instance 的 instanceof. 然后是 assignment.注意 ts 总是会利用初始化时的 type 来进行检查. 然后在 control flow 中 ts 会自动检查上面的代码对下方的 type 的影响. 我们还可以自己设置函数来检查 type 以便于narrowing:

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

其中的pet is Fish被称为type predicate. 当每一个 type 均包含一个相同的是 literal type 的 property 时,union 被称为discriminated union,ts 会自动利用该 property 进行narrowing:

interface Circle {
  kind: "circle";

  radius: number;
}
interface Square {
  kind: "square";

  sideLength: number;
}
type Shape = Circle | Square;

function getArea(shape: Shape) {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius ** 2; //shape:Circle
  }
}

ts 还设置了never,用于说明在这种情况下该变量不可能是任何类型:

type Shape = Circle | Square;

function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;

    case "square":
      return shape.sideLength ** 2;

    default:
      const _exhaustiveCheck: never = shape;

      return _exhaustiveCheck;
  }
}

More on Functions

表示函数的 type:

function greeter(fn: (a: string) => void) {
  fn("Hello, World");
}

注意若进行类型注释,参数名是必须的. 在 object type 中的函数使用 call signature:

type DescribableFunction = {
  description: string;

  (someArg: number): boolean;
};

对于 constructor,使用construct signature:

type SomeConstructor = {
  new (s: string): SomeObject;
};

对于有些即可作为 constructor,也可以不作为:

interface CallOrConstruct {
  new (s: string): Date;

  (n?: number): number;
}

在 ts 中,generics用与表示多个值的 type 的对应关系:

function firstElement<Type>(arr: Type[]): Type | undefined {
  return arr[0];
}

其中的Type称为Type parameter,它的命名是任意的.可以有多个:

function map<Input, Output>(
  arr: Input[],
  func: (arg: Input) => Output
): Output[] {
  return arr.map(func);
}

我们可以对type parameter添加限制条件:

function longest<Type extends { length: number }>(a: Type, b: Type) {
  if (a.length >= b.length) {
    return a;
  } else {
    return b;
  }
}

// longerArray is of type 'number[]'

const longerArray = longest([1, 2], [1, 2, 3]);

// longerString is of type 'alice' | 'bob'

const longerString = longest("alice", "bob");

// Error! Numbers don't have a 'length' property

const notOK = longest(10, 100);

使用时可以指定type parameter的值:

const arr = combine<string | number>([1, 2, 3], ["hello"]);

写出好的type parameter因遵循如下条件:

  1. 使用 type parameter 时尽量不适应限制条件.
  2. 使用尽可能少的 type parameter.
  3. 如果 type parameter 在函数中只出现了一次,重新思考是否需要它. 我们使用?表示函数某个参数可选:
function f(x?: number) {
  // ...
}

f(); // OK

f(10); // OK

当然我们也可以使用参数带默认值实现. 对于 callback,最好不要使用可选参数,除非需要一个不使用该参数的函数. 我们可以使用overload signatures来定义可以用多种方式调用的函数,它由一系列function signature加上函数的主体组成:

function makeDate(timestamp: number): Date;

function makeDate(m: number, d: number, y: number): Date;

function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
  if (d !== undefined && y !== undefined) {
    return new Date(y, mOrTimestamp, d);
  } else {
    return new Date(mOrTimestamp);
  }
}

const d1 = makeDate(12345678);

const d2 = makeDate(5, 5, 5);

const d3 = makeDate(1, 3); //error

注意function signature需要至少有两个. 为写好 overload,如果可以用 union 参数代替,使用 union 参数. 我们可以指定 this 的 type(利用在 js 中 this 不能作为参数名的机制):

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

此时如果我们使用 arrow function 就会报错,因为 arrow function 的 this 是 globalThis. 我们使用void表示函数不返回值,注意它与undefined不同. 此时函数仍可返回值,不过将函数返回值赋给变量时,它们的 type 仍会是void.这个特性的利用例子:

const src = [1, 2, 3];

const dst = [0];

src.forEach((el) => dst.push(el)); //需要一个返回void的函数但push返回数字.

我们使用object表示除 primitives 外的 type,注意它和 js 中的Object不同. 我们使用unknown表示任何类型,不过与 any 不同的是它不需要进行任何操作. 我们使用never表示在函数中不能返回值,即中止程序或抛出异常. 我们使用Function表示没有具体 type 的函数.应避免使用它. 指定rest parameter的 type 必须是 array type:

function multiply(n: number, ...m: number[]) {
  return m.map((x) => n * x);
}

我们可以使用rest argument来展开数组作为参数:

const arr1 = [1, 2, 3];

const arr2 = [4, 5, 6];

arr1.push(...arr2);

但注意 ts 不认为数组不可修改,故有时会遇到问题:

const args = [8, 5];

const angle = Math.atan2(...args); //parameter:x:number,y:number

解决方法:

// Inferred as 2-length tuple

const args = [8, 5] as const;

// OK

const angle = Math.atan2(...args);

在低版本的运行环境使用rest argument可能需要打开 downlevelIteration. 我们可以在parameter destructuring指定 type:

function sum({ a, b, c }: { a: number; b: number; c: number }) {
  console.log(a + b + c);
}

Object Types

在 ts 中,object 的类型(object types)可以是匿名的:

function greet(person: { name: string; age: number }) {
  return "Hello " + person.name;
}

也可以通过interfacetype alias命名:

interface Person {
  name: string;

  age: number;
}

对于 object types 中的 properties,我们可以进行三种操作:

  1. 标明 type
  2. 表示某个 property 可选
  3. 表示某个 property 只读 标明 type 上面已有例子,标明可选:
interface PaintOptions {
  shape: Shape;

  xPos?: number;

  yPos?: number;
}

可使用 destruction 语法给可选 property 设置默认值:

function paintShape({ shape, xPos = 0, yPos = 0 }: PaintOptions) {

	...

}

注意在 destruction 不可使用 type annotation,因为在 js 的 destruction 语法中注释的语法已有其他的含义. 标明只读:

interface SomeType {
  readonly prop: string;
}

注意只读的只有 prop 本身,它内部的 property 等仍可读写. ts 并不会因为readonly影响 type 的判断,故以下语句是合法的:

interface Person {
  name: string;

  age: number;
}

interface ReadonlyPerson {
  readonly name: string;

  readonly age: number;
}

let writablePerson: Person = {
  name: "Person McPersonface",

  age: 42,
};

// works

let readonlyPerson: ReadonlyPerson = writablePerson;

console.log(readonlyPerson.age); // prints '42'

writablePerson.age++;

console.log(readonlyPerson.age); // prints '43'

当 object 本身仅部分确定时,我们可以使用index signature来表示可能值的 type:

interface StringArray {
  [index: number]: string;
}

const myArray: StringArray = getStringArray();

const secondItem = myArray[1];

index 的类型只能是 number 或 string(可以两者一起).index signature也可以添加readonly. 它可以和一些 properties 的 type annotation 组合在一起,但这些 properties 必须符合index signature的规定:

interface NumberDictionary {
  [index: string]: number;

  length: number; // ok

  name: string; //error
}

其中 index 相关的 property 必须通过[]调用. 有时我们希望从现有的 type 的基础上新增 properties 参数新的 type,使用extend:

interface BasicAddress {
  name?: string;

  street: string;

  city: string;

  country: string;

  postalCode: string;
}

interface AddressWithUnit extends BasicAddress {
  unit: string;
}

可以extend多个 type. 我们也可以使用intersection types来组合现有的 type:

interface Colorful {
  color: string;
}

interface Circle {
  radius: number;
}

type ColorfulCircle = Colorful & Circle;

我们可以在 object types 中使用generic:

interface Box<Type> {
  contents: Type;
}

let box: Box<string>;

type alias也可以使用,它可以拓展除很多其他用法:

type OrNull<Type> = Type | null;

type OneOrMany<Type> = Type | Type[];

在 ts 中的Array本身就是一个 generic type. 除了正常的数组,我们可以创建只读数组:

function doStuff(values: ReadonlyArray<string>) {
  // We can read from 'values'...

  const copy = values.slice();

  console.log(`The first value is ${values[0]}`);

  // ...but we can't mutate 'values'.

  values.push("hello!"); //error!
}

注意并没有叫ReadonlyArray的 constructor,不过我们可以把正常的数组赋值给它:

const roArray: ReadonlyArray<string> = ["red", "green", "blue"];

注意反过来就不可行:

let x: readonly string[] = [];

let y: string[] = [];

x = y;

y = x; //error!

类似Array,它也有一个 shorthand 语法:readonly Type[]. 当我们对数组的形式有明确认知时,可以使用tuple types:

type StringNumberPair = [string, number];

此时它的 length 已确定,当赋值时会检查 length. 可以有可选的 properties:

type Either2dOr3d = [number, number, number?];

也可以不确定 length,而且指定一部分值的类型:

type StringNumberBooleans = [string, number, ...boolean[]];

type StringBooleansNumber = [string, ...boolean[], number];

type BooleansStringNumber = [...boolean[], string, number];

它可以加上readonly,注意如果普通数组使用了const assertion,它会被视为readonly tuple type:

let point = [3, 4] as const;

Type manipulation

generic

我们可以interface来形成约束条件:

interface Lengthwise {
  length: number;
}

function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
  console.log(arg.length); // Now we know it has a .length property, so no more error

  return arg;
}

也可以使用其他的type parameter:

function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
  return obj[key];
}

想使用generic建立一个创建 class 实例的函数,需要指向它的构造函数:

function create<Type>(c: { new (): Type }): Type {
  return new c();
}

keyof

对 object type 使用使用keyof获得一个它的 key 组成的 union:

type Point = { x: number; y: number };

type P = keyof Point; //P:"x"|"y"

如果使用了index signature,则会是 index 对于的 type:

type Arrayish = { [n: number]: unknown };

type A = keyof Arrayish;

//type A :number

type Mapish = { [k: string]: boolean };

type M = keyof Mapish;

//type M:string|number,因为js中的obj会自动将obj[0]转化为obj["0"]

typeof

在 js 中已经中typeof运算符可已使用,在 ts 中它能正确根据上下文推断出 type:

let s = "hello";

let n: typeof s; //n:string

这在使用如ReturnType这类内建 type 时很有用:

type Predicate = (x: unknown) => boolean;

type K = ReturnType<Predicate>; //K:boolean,ReturnType只接受type参数

注意当作为 type annotation 使用时,只有对identifiers(如变量名,函数名)和它们的 properties 中使用typeof才合法.

indexed access types

我们可以使用index来获取 type:

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

type Age = Person["age"]; //type Age = number

index本身接受一个 type,故可以使用 type 的各种特性:

type I1 = Person["age" | "name"];

type I1 = string | number;

type I2 = Person[keyof Person];

我们可以使用number获取数组的 type:

const MyArray = [
  { name: "Alice", age: 15 },

  { name: "Bob", age: 23 },

  { name: "Eve", age: 38 },
];

type Person = (typeof MyArray)[number];

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

type Age = (typeof MyArray)[number]["age"];

type Age = number;

// Or

type Age2 = Person["age"];

type Age2 = number;

注意能作为index的只有 type,故以下语句是非法的:

const key = "age";

type Age = Person[key];

condition types

我们可以使用condition types来利用条件判断判断 type:

interface Animal {
  live(): void;
}

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

type Example1 = Dog extends Animal ? number : string; //number

type Example2 = RegExp extends Animal ? number : string; //string

我们可以使用infer来利用条件推断出正确时的 type:

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

type Num = GetReturnType<() => number>;
//type Num = number

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

如果定义了多个call signatures,以最后一个为准:

declare function stringOrNum(x: string): number;

declare function stringOrNum(x: number): string;

declare function stringOrNum(x: string | number): string | number;

type T1 = ReturnType<typeof stringOrNum>; //string | number

如果与generic联合时使用了 union,union 的每一个内容都会被应用一次:

type ToArray<Type> = Type extends any ? Type[] : never;

type StrArrOrNumArr = ToArray<string | number>; //string[] | number[]

取消这种默认行为:

type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;

// 'StrArrOrNumArr' is no longer a union.

type StrArrOrNumArr = ToArrayNonDist<string | number>; //(string | number)[]

mapped types

我们有时希望通过现有的 type 建立新的 type,此时我们可以使用mapped types,遍历某个 type 的 key 建立 type:

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

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

  newUserProfile: () => void;
};

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

它可以加上mapping modifiers:readonly 和?,加上前缀,控制加上(+)或移除(-)这些功能.不加前缀的话,默认是+.

// Removes 'readonly' attributes from a type's properties

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

// Removes 'optional' attributes from a type's properties

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

我们还可以使用as进行第二次匹配:

// Remove the 'kind' property

type RemoveKindField<Type> = {
  [Property in keyof Type as Exclude<Property, "kind">]: Type[Property];
};
//Exclude<T,U>在T中有U时返回never,没有返回T.

interface Circle {
  kind: "circle";

  radius: number;
}

type KindlessCircle = RemoveKindField<Circle>;
//type KindlessCircle = { radius: number; }

template literal types

就像 js 中的 template literal,该语法也可以被用于 type:

type World = "world";

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

如果我们使用 union,则 union 中的每一个内容都会应用一次形成新的 union:

type EmailLocaleIDs = "welcome_email" | "email_heading";

type FooterLocaleIDs = "footer_title" | "footer_sendoff";

type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;

//type AllLocaleIDs = "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"

如果使用了多个 union,则它们会交叉应用:

type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;

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

type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`;
//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"

为 string 类型 type 的操作,ts 内置了一些 types 可以被使用:

  • Uppercase<StringType>:全部转为大写字母.
  • Lowercase<StringType>:全部转为小写字母.
  • Capitalize<StringType>:首字母大写.
  • Uncapitalize<StringType>:首字母小写.

class

我们使用field declaration来声明 public 的可读写的 properties:

class Point {
  x: number;

  y: number;
}

可以初始化:

class Point {
  x = 0;

  y = 0;
}

在声明了相应的 type 后便不可改变. properties 可加上readonly设置为只读. 加上constructor:

class Point {
  x: number;

  y: number;

  // Normal signature with defaults

  constructor(x = 0, y = 0) {
    this.x = x;

    this.y = y;
  }
}

constructor与一般的函数的区别是:

  • 不可用使用 type parameter.
  • 不可以对 return type 使用 type annotation. 进行继承时,如果在constructor没有进行super(),ts 会自动报错. 添加 method:
class Point {
  x = 10;

  y = 10;

  scale(n: number): void {
    this.x *= n;

    this.y *= n;
  }
}

注意使用 properties 要加上 this 进行限定. 使用gettersetter:

class C {
  _length = 0;

  get length() {
    return this._length;
  }

  set length(value) {
    this._length = value;
  }
}

有几点需要注意:

  • 如果只设置了 getter,则 property 会自动被认为是 readonly.
  • 如果 setter 的参数 type 没有指定,ts 会自动从 getter 的返回值推断.
  • getter 和 setter 必须具有相同的 member visibility(即 public 等). class 可以使用index signature:
class MyClass {
  [s: string]: boolean | ((s: string) => boolean);

  check(s: string) {
    return this[s] as boolean;
  }
}

我们可以使用implement来检查 class 是否符合某个 interface 定义的 type:

interface Pingable {
  ping(): void;
}

class Sonar implements Pingable {
  ping() {
    console.log("ping!");
  }
}

//error
class Ball implements Pingable {
  pong() {
    console.log("pong!");
  }
}

注意这仅仅只是进行检查,并不会改变 class,故相应的 type annotation 还是需要做:

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

class NameChecker implements Checkable {
  check(s) {
    //s:any

    // Notice no error here

    return s.toLowercse() === "ok";
  }
}

而且如果其中有可选的 property,也不会自动创建将 property. 继承时覆盖 method:

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

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

注意继承的 class 必须遵循原来的 class,这样的有点是下面的语句合法:

const b: Base = d; //d是继承的class的instance

// No problem

b.greet();

如果不遵循就会出错:

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

class Derived extends Base {
  // Make this parameter required,error!!!

  greet(name: string) {
    console.log(`Hello, ${name.toUpperCase()}`);
  }
}

当 target>=ES2022 或 useDefineForClassFields被设置为 true 时,class fields 会在父 class 的 constructor 运行完后运行,覆盖父 class 的 properties,但有时我们仅仅只是想声明一个更具体的派生除了的 type 而不是覆盖,这时使用declare防止覆盖:

interface Animal {
  dateOfBirth: any;
}

interface Dog extends Animal {
  breed: any;
}

class AnimalHouse {
  resident: Animal;

  constructor(animal: Animal) {
    this.resident = animal;
  }
}

class DogHouse extends AnimalHouse {
  // Does not emit JavaScript code,

  // only ensures the types are correct

  declare resident: Dog;

  constructor(dog: Dog) {
    super(dog);
  }
}

class 的初始化的运行顺序如下:

  • base class field 初始化.
  • base class constructor 运行.
  • derived class fields 初始化.
  • derived class constructor 运行. 这有时不注意的话会导致奇怪的结果:
class Base {
  name = "base";

  constructor() {
    console.log("My name is " + this.name);
  }
}

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

// Prints "base", not "derived"

const d = new Derived();

当继承内置的 type 时,若编译到 ES6 之前的版本,可能引发问题. 我们可以使用各种关键词设置member visibility,即控制 class 的内容对外部可见不可见. 如果没指定,默认是public,即对外部可见:

class Greeter {
  public greet() {
    console.log("hi!");
  }
}

const g = new Greeter();

g.greet();

使用protected对子类和自身内部可见:

class Greeter {
  protected getName() {
    return "hi";
  }
}

class SpecialGreeter extends Greeter {
  public howdy() {
    // OK to access protected member here

    console.log("Howdy, " + this.getName());
  }
}

const g = new SpecialGreeter();

g.getName(); //error

我们可以在继承的 class 中将protected的内容暴露给外部:

class Base {
  protected m = 10;
}

class Derived extends Base {
  // No modifier, so default is 'public'

  m = 15;
}

const d = new Derived();

console.log(d.m); // OK

不可以使用 base class 的实例在子类中访问protected的内容:

class Base {
  protected x: number = 1;
}

class Derived1 extends Base {
  protected x: number = 5;
}

class Derived2 extends Base {
  f1(other: Derived2) {
    other.x = 10;
  }

  f2(other: Base) {
    other.x = 10; //error
  }
}

使用private使得仅对内部可见:

class Base {
  private x = 0;
}

const b = new Base();

// Can't access from outside the class

console.log(b.x);

允许在 class 内通过其他 instance 内访问:

class A {
  private x = 10;

  public sameAs(other: A) {
    // No error

    return other.x === this.x;
  }
}

注意protectedprivate仅仅是 ts 的内容,在编译出的 js 文件中相关的 class 的内容仍可被访问,不过如果使用 js 的 private field(#),在编译后依然能禁止访问. 在 ts 中,不可使用static覆盖Function properties:

class S {
  static name = "S!"; //error!
}

private等则可正常使用. class 可以使用generic:

class Box<Type> {
  contents: Type;

  constructor(value: Type) {
    this.contents = value;
  }
}

const b = new Box("hello!");

static的内容不可使用type parameter. 在 ts 中,method 可以使用arrow function,它会被保证正确得到this:

class MyClass {
  name = "MyClass";

  getName = () => {
    return this.name;
  };
}

const c = new MyClass();

const g = c.getName;

// Prints "MyClass" instead of crashing

console.log(g());

但有以下确定:

  • 使用了更多的内存.
  • 不可以通过 super 访问到. 我们可以设置this参数来保证 class 的函数被正确使用:
class MyClass {
  name = "MyClass";

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

const c = new MyClass();

// OK

c.getName();

// Error, would crash

const g = c.getName;

console.log(g());

在 ts 中,this也是一个特殊的表示当前 class 的 type:

class Box {
  contents: string = "";

  set(value: string) {
    this.contents = value;

    return this;
  }
}
class ClearableBox extends Box {
  clear() {
    this.contents = "";
  }
}

const a = new ClearableBox();

const b = a.set("hello");
//const b: ClearableBox

this 也可以被利用来进行 narrowing:

class FileSystemObject {
  isFile(): this is FileRep {
    return this instanceof FileRep;
  }

  isDirectory(): this is Directory {
    return this instanceof Directory;
  }

  isNetworked(): this is Networked & this {
    return this.networked;
  }

  constructor(public path: string, private networked: boolean) {}
}

class FileRep extends FileSystemObject {
  constructor(path: string, public content: string) {
    super(path, false);
  }
}

class Directory extends FileSystemObject {
  children: FileSystemObject[];
}

interface Networked {
  host: string;
}

const fso: FileSystemObject = new FileRep("foo/bar.txt", "foo");

if (fso.isFile()) {
  fso.content;
} else if (fso.isDirectory()) {
  fso.children;
} else if (fso.isNetworked()) {
  fso.host;
}

我们可以使用abstract的 classes 和 members 用于继承,继承时需要将abstract的内容具体实现:

abstract class Base {
  abstract getName(): string;

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

const b = new Base(); //error

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

const d = new Derived();

d.printName();

当想接受abstract的 class 作为参数时,需要加上一些限制:

function greet(ctor: new () => Base) {
  const instance = new ctor();

  instance.printName();
}

greet(Derived);

greet(Base); //error

除此之外注意Empty class会被判定符合所有 classes:

function fn(x: Empty) {
  // can't do anything with 'x', so I won't
}

// All OK!

fn(window);

fn({});

fn(fn);

Modules

在 ts 中,type 可以被正常importexport.不过我们也可以使用 ts 添加的方法表示我们在处理 type:

import type { Cat, Dog } from "./animal.js";
import { createCatName, type Cat, type Dog } from "./animal.js";

设置

strict:开启一系列增强的类型检查功能. noImplicitAny:禁止 ts 中出现any类型. strictNullChecks:更严格地检查 null 和 undefined. strictPropertyInitialization:强制 class 的 property 初始化(在 field declaration 或 constructor).可使用name!: string;来不初始化且避免错误. target:表示要支持到的 js 运行环境,高于该环境的版本的代码会被转化. module:设置 modules 之间使用什么通信.