前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >TS 进阶 - 类型基础

TS 进阶 - 类型基础

作者头像
Cellinlab
发布2023-05-17 20:36:21
发布2023-05-17 20:36:21
1.8K00
代码可运行
举报
文章被收录于专栏:Cellinlab's BlogCellinlab's Blog
运行总次数:0
代码可运行

# 学习方法

# 最佳学习路径

  • 类型
  • 语法
  • 工程

# VS Code 插件

  • TypeScript Importer
    • : 类型补全
    • 自动导入
  • Move TS
    • 编辑文件路径,直接修改目录结构
  • Error Lens
    • 错误提示

# 快速练习

# TS 文件快速执行

ts-node

  • 安装
代码语言:javascript
代码运行次数:0
运行
复制
npm i ts-node typescript -g

  • 在项目中创建项目配置文件 tsconfig.json
代码语言:javascript
代码运行次数:0
运行
复制
npx --package typescript tsc --init

# 如果已经全局安装 TypeScript
tsc --init

  • 创建 TS 文件 index.ts
代码语言:javascript
代码运行次数:0
运行
复制
console.log('Hello TypeScript')

  • 执行
代码语言:javascript
代码运行次数:0
运行
复制
ts-node index.ts

  • 配置参数
    • -P, --project 指定 tsconfig 文件位置,默认 ./tsconfig.json
    • -T, --transpileOnly 只编译,不检查类型
    • --swctranspileOnly 基础上使用 swc 进行文件编译,进一步提升执行速度
    • --emit 执行并且生成 JS 文件,输出到 .ts-node 文件夹下(需要与 --compilerHost 一起使用)

# 原型类型和对象类型

# 原始类型标注

代码语言:javascript
代码运行次数:0
运行
复制
const name: string = 'Cell';
const age: number = 18;
const isMale: boolean = false;
const undef: undefined = undefined;
const nul: null = null;
const obj: object = {};
const bigint: bigint = BigInt(1);
const symbol: symbol = Symbol();

# null 和 undefined

  • null 有值,但是个空值
  • undefined 没有值

在 TypeScript 中,nullundefined 类型是有具体意义的类型。在没有开启 strictNullChecks 检查时,会被视为其他类型的子类型,如 string 类型会被认为包含了 nullundefined

代码语言:javascript
代码运行次数:0
运行
复制
const tmp1: null = null;
const tmp2: undefined = undefined;

const tmp3: string = null; // 在关闭 strictNullChecks 检查时,不会报错
const tmp4: string = undefined;

# void

JavaScript 中 void 操作符会执行后面跟着的表达式并返回一个 undefined

代码语言:javascript
代码运行次数:0
运行
复制
void function iife() {
  console.log('Invoke Immediately')
}(); // undefined

// 相当于
void((function iife(){
  console.log('Invoke Immediately')
})());

TypeScript 中的原始类型标注 void,用于描述一个内部没有 return 语句 或 没有显示 return 一个值的函数的返回值类型。

代码语言:javascript
代码运行次数:0
运行
复制
function fn1 () {}
function fn2 () {
  return;
}
function fn3 () {
  return undefined;
}

上面 fn1()fn2() 的返回值类型都会被隐式推导为 void,只有显式返回了 undefined 值的 fn3() 其返回值类型才被推导为 undefined。但是,在实际执行过程中,fn1()fn2() 的返回值都是 undefined

虽然 fn3() 返回值类型会被推导为 undefined,但仍然可以使用 void 类型进行标注,因为在类型层面 fn1()fn2()fn3() 都表示“没有返回一个有意义的值”。

# 数组类型标注

代码语言:javascript
代码运行次数:0
运行
复制
const arr1: number[] = [1, 2, 3];
const arr2: Array<number> = [1, 2, 3];

# 元组

代码语言:javascript
代码运行次数:0
运行
复制
const arr3: string[string, string, string] = ['a', 'b', 'c'];
const arr4: [string, number] = ['a', 1];

// 可选成员
const arr5: [string, number?] = ['a'];
// 可选成员的长度属性类型
type TupleLength = typeof arr5.length; // 1 | 2

具名元组

代码语言:javascript
代码运行次数:0
运行
复制
const arr6: [name: string, age: number] = ['Cell', 18];
const arr7: [name: string, age: number, male?: boolean] = ['Cell', 18, true];

相对于数组,使用元组能帮助进一步提升数据结构的严谨性,包括基于位置的类型标注,避免出现越界访问等。

# 对象类型标注

TypeScript 中需要特殊的类型标注来描述对象类型——interface,其代表了对象对外提供的接口结构。

代码语言:javascript
代码运行次数:0
运行
复制
interface IDescription {
  name: string;
  age: number;
  male: boolean;
}

const p1: IDescription = {
  name: 'Cell',
  age: 18,
  male: true
};

对对象的描述:

  • 每一个属性的值必须一一对应到接口的属性类型
  • 不能有多的属性,也不能有少的属性。包括在对象内部直接声明,或 obj.prop 属性访问赋值的形式

# 修饰接口属性

代码语言:javascript
代码运行次数:0
运行
复制
interface IDescription {
  name: string;
  age: number;
  male?: boolean;
  func?: Function;
}

const p2: IDescription = {
  name: 'Cell',
  age: 18,
  male: true,
  // func 也可以不实现
}

// 可选函数类型属性只限制不能调用,不会影响赋值
p2.func = () => {};

只读属性

代码语言:javascript
代码运行次数:0
运行
复制
interface IDescription {
  readonly id: number;
  name: string;
  age: number;
}

const p3: IDescription = {
  id: 1,
  name: 'Cell',
  age: 18
}

p3.id = 2; // Error: Cannot assign to 'id' because it is a read-only property.

  • 数组与元组层面也有只读的修饰
  • 不过只能将整个数组或元组标记为只读,不能想对象标记特定属性
  • 一旦被标记只读,那被标记的数组或元组类型上,将不再有 pushpop 等方法
    • 本质是只读数组或元组的类型实际上变成了 ReadonlyArray 而不是 Array

# type 与 interface

虽然 type 也可以代替 interface 描述对象,但更推荐用 interface 来描述对象、类的结构,而类型别名用来将一个函数签名、一组联合类型、一个工具类型等抽离成一个完整独立的类型

# object、Object 和 {}

Object

  • 原型链的顶端是 Object 以及 Function,所有的原始类型与对象类型最终都指向 Object,在 TypeScript 中表现为 Object 包含了所有类型
代码语言:javascript
代码运行次数:0
运行
复制
const tmp1: Object = undefined;
const tmp2: Object = null;
const tmp3: Object = void 0;

const tmp4: Object = 'Cell';
const tmp5: Object = 2022;
const tmp6: Object = true;
const tmp7: Object = { name: 'Cell' };
const tmp8: Object = [1, 2, 3];
const tmp9: Object = () => {};

  • Object 类似的还有 BooleanNumberStringSymbol,这些装箱类型同样包含了一些超出预期的类型
代码语言:javascript
代码运行次数:0
运行
复制
// String 同样包括 undefined、null、void 及其拆箱类型 string
const tmp1: String = undefined;
const tmp2: String = null;
const tmp3: String = void 0;
const tmp4: String = 'Cell';

任何情况下,都不应该使用这些装箱类型

object

  • object 的引入就是为了解决 Object 类型的错误使用,它代表所有非原始类型的类型,即数组、对象和函数类型
代码语言:javascript
代码运行次数:0
运行
复制
const tmp1: object = undefined; // Error: Type 'undefined' is not assignable to type 'object'.
const tmp2: object = null; // Error: Type 'null' is not assignable to type 'object'.
const tmp3: object = void 0; // Error: Type 'void' is not assignable to type 'object'.

const tmp4: object = 'Cell'; // Error: Type 'string' is not assignable to type 'object'.
const tmp5: object = 2022; // Error: Type 'number' is not assignable to type 'object'.
const tmp6: object = true; // Error: Type 'boolean' is not assignable to type 'object'.

const tmp7: object = { name: 'Cell' };
const tmp8: object = [1, 2, 3];
const tmp9: object = () => {};

{}

  • {} 对象字面量类型(对应字符串字面量类型那种)
  • 可以使用 {} 作为类型签名,一个内部无属性定义的空对象,类似于 Object,接受任何非 nullundefined 的值
代码语言:javascript
代码运行次数:0
运行
复制
const tmp1: {} = undefined; // Error: Type 'undefined' is not assignable to type '{}'.
const tmp2: {} = null; // Error: Type 'null' is not assignable to type '{}'.
const tmp3: {} = void 0; // Error: Type 'void' is not assignable to type '{}'.

const tmp4: {} = 'Cell';
const tmp5: {} = 2022;
const tmp6: {} = true;
const tmp7: {} = { name: 'Cell' };
const tmp8: {} = [1, 2, 3];
const tmp9: {} = () => {};

  • 虽然可以将其作为变量的类型,但实际上无法对这个变量进行任何赋值操作
代码语言:javascript
代码运行次数:0
运行
复制
const tmp: {} = { name: 'Cell' };
tmp.name = 'Cellinlab'; // Error: Property 'name' does not exist on type '{}'.
tmp.age = 18; // Error: Property 'age' does not exist on type '{}'.

  • 在任何时候不要使用 Object 及类似的装箱类型
  • 在不确定某个变量的具体类型,但能确定其不是原始类型时,可以使用 object
    • 推荐还是进行更一步区分
      • Record<string, unknown>Record<string, any> 代表对象
      • unknown[]any[] 代表数组
      • (...args: any[]) => any 代表函数
  • 避免使用 {}
    • {} 意味任何非 null / undefined 的值,使用它和使用 any 一样恶劣

# Symbol

Symbol 在 JavaScript 中代表一个唯一的值类型,类似于字符串类型,可以作为对象的属性名,并用于避免错误修改 对象或 class 内部属性的情况。

在 TypeScript 中,symbol 类型并不具有这一特性,多个具有 symbol 类型的对象,它们的 symbol 类型指的都是 TypeScript 中的同一个类型。

为了实现“独一无二”特性,TypeScript 中支持了 unique symbol 类型声明,它是 symbol 类型的子类型,每一个 unique symbol 类型都是独一无二的。

代码语言:javascript
代码运行次数:0
运行
复制
const sym1: unique symbol = Symbol();

const sym2: unique symbol = sym1; // Error: Type 'typeof sym1' is not assignable to type 'typeof sym2'.

在 JavaScript 中,可以用 Symbol.for 方法来复用已创建的 Symbol,如 Symbol.for('Cell') 会首先查找全局是否已经有使用 Cell 作为 keySymbol 注册,如果有则返回该 Symbol,否则创建一个新的 Symbol 并注册到全局。

在 TypeScript 中,要引用已创建的 unique symbol 类型,需要使用类型查询操作符 typeof,如 typeof sym1

代码语言:javascript
代码运行次数:0
运行
复制
declare const uniqueSymbolFoo: unique symbol;

const uniqueSymbolBaz: typeof uniqueSymbolFoo = uniqueSymbolFoo;

# 字面量类型

# 字面量类型和联合类型

代码语言:javascript
代码运行次数:0
运行
复制
interface Res {
  code: 10000 | 10001 | 50000;
  status: 'success' | 'error';
  data: any;
}

# 字面量类型

字面量类型,代表比原始类型更精确的类型,同时原始类型的子类型。

字面量类型主要包括字符串字面量类型数字字面量类型布尔字面量类型对象字面量类型

代码语言:javascript
代码运行次数:0
运行
复制
const tmp1: 'Cell' = 'Cell';
const tmp2: 2022 = 2022;
const tmp3: true = true;
const tmp4: { name: 'Cell' } = { name: 'Cell' };

原始类型的值可以包括任意的同类型值,而字面量类型要求是值级别的字面量一致

代码语言:javascript
代码运行次数:0
运行
复制
const tmp1: 'Cell' = 'Cellinlab'; // Error: Type '"Cellinlab"' is not assignable to type '"Cell"'.

const tmp2: 2022 = 2021; // Error: Type '2021' is not assignable to type '2022'.

const tmp3: true = false; // Error: Type 'false' is not assignable to type 'true'.

const tmp4: string = 'Cell';
const tmp5: number = 2022;
const tmp6: boolean = true;

单独使用字面量类型比较少见,因为单个字面量类型并没有什么实际意义。通常和联合类型(|)一起使用,表达一组字面量类型:

代码语言:javascript
代码运行次数:0
运行
复制
interface Tmp {
  bool: true | false;
  num: 1 | 2 | 3;
  str: 'Cell' | 'Cellinlab';
}

# 联合类型

联合类型,代表一组类型的可用集合,只要最终赋值的类型属于联合类型的一员,就可以通过类型检查。

联合类型对其成员并没有任何限制,除了对同一类型字面量的联合,还可以将各种类型混合到一起:

代码语言:javascript
代码运行次数:0
运行
复制
interface Tmp {
  mixed: 'Cell' | 2022 | true | {} | [1, 2, 3] | (() => {}) | (1 | 2 | 3);
}

联合类型使用是需要注意:

  • 对于联合类型中的函数类型需要使用 () 包裹起来
  • 函数类型并不存在字面量类型,因此 (() => {}) 是一个合法的函数类型
  • 可以在联合类型中进一步嵌套联合类型,这些嵌套的联合类型最终都会被展平到第一级

联合类型常用场景之一是通过多个对象类型的联合,来实现手动的互斥属性,即这一属性如果有 字段1 那就没有字段2:

代码语言:javascript
代码运行次数:0
运行
复制
interface Tmp {
  user:
  | {
    vip: true;
    expire: number;
  }
  | {
    vip: false;
    promotion: string;
  };
}

declare var tmp: Tmp;

if (tmp.user.vip) {
  console.log(tmp.user.expire);
} else {
  console.log(tmp.user.promotion);
}

也可以通过类型别名来复用一组字面量联合类型

代码语言:javascript
代码运行次数:0
运行
复制
type Code = 10000 | 10001 | 50000;
type Status = 'success' | 'error';

# 对象字面量类型

对象字面量类型就是一个对象类型的值,即这个对象的值全都为字面量值:

代码语言:javascript
代码运行次数:0
运行
复制
interface Tmp {
  obj: {
    name: 'Cell';
    age: 18;
  };
}

const tmp: Tmp = {
  obj: {
    name: 'Cell',
    age: 18,
  },
};

无论是原始类型还是对象类型的字面量类型,其本质都是类型而不是值。在编译时同样会被移除,同时也是被存储在内存中的类型空间而非值空间。

# 枚举

如果说字面量类型是对原始类型的进一步扩展,那么某些方面枚举类型就是对对象类型的进一步扩展。

代码语言:javascript
代码运行次数:0
运行
复制
enum PageUrl {
  Home_Page_Url = '/',
  Setting_Page_Url = '/setting',
  About_Page_Url = '/about',
}

const home = PageUrl.Home_Page_Url;

枚举在提供更好的类型提示之外,还将这些常量真正地约束在一个命名空间下。

如果没有声明枚举的值,它会默认使用数字枚举,且默认值从 0 开始:

代码语言:javascript
代码运行次数:0
运行
复制
enum Items {
  A,
  B,
  C,
}

console.log(Items.A); // 0
console.log(Items.B); // 1
console.log(Items.C); // 2

如果只为某个成员指定了枚举值,其之前的成员仍从 0 开始,之后的成员会从指定值递增:

代码语言:javascript
代码运行次数:0
运行
复制
enum Items {
  A,
  B = 10,
  C,
}

console.log(Items.A); // 0
console.log(Items.B); // 10
console.log(Items.C); // 11

在数字型枚举中,可以使用延迟求值的枚举值,如函数:

代码语言:javascript
代码运行次数:0
运行
复制
const returnNum = () => 2021 + 1;

enum Items {
  A,
  B = returnNum(),
}

console.log(Items.A); // 0
console.log(Items.B); // 2022

如果使用了延迟求值,那么没有使用延迟求值的枚举成员必须放在使用常量枚举值声明的成员之后,或者放在第一位。

TypeScript 中可以同时使用字符串枚举值和数字枚举值:

代码语言:javascript
代码运行次数:0
运行
复制
enum Mixed {
  Num = 1,
  Str = 'str',
}

枚举和对象的重要差异在于,对象是单向映射的,只能从键映射到键值,而枚举是双向映射的,可以从枚举成员映射到枚举值,也可以从枚举值映射到枚举成员。

代码语言:javascript
代码运行次数:0
运行
复制
enum Items {
  A,
  B,
  C,
}

console.log(Items.A); // 0
console.log(Items[0]); // A

// 原理
// "use strict";
// var Items;
// (function (Items) {
//     Items[Items["A"] = 0] = "A";
//     Items[Items["B"] = 1] = "B";
//     Items[Items["C"] = 2] = "C";
// })(Items || (Items = {}));

注意,仅有值为数字的枚举成员才能进行双向枚举,字符串成员仍然只会进行单次映射

代码语言:javascript
代码运行次数:0
运行
复制
enum Items {
  A,
  B = 'b',
  C = 'c',
}

// "use strict";
// var Items;
// (function (Items) {
//     Items[Items["A"] = 0] = "A";
//     Items["B"] = "b";
//     Items["C"] = "c";
// })(Items || (Items = {}));

# 常量枚举

常量枚举和枚举类似,只是声明多了一个 const

代码语言:javascript
代码运行次数:0
运行
复制
const enum Items {
  A,
  B,
  C,
}

console.log(Items.A); // 0
// console.log(Items[0]); // Error A const enum member can only be accessed using a string literal.

// "use strict";
// console.log(0 /* Items.A */); // 0

常量枚举只能通过枚举成员访问枚举值,同时,其编译产物中并不会存在一个额外的辅助对象,对枚举成员的访问会被直接内联替换为枚举的值。

# 函数

# 函数的类型签名

函数的类型描述函数入参类型和函数返回值类型。

代码语言:javascript
代码运行次数:0
运行
复制
// 函数声明
function foo(name: string): number {
  return name.length;
}

// 函数表达式
const foo2 = function (name: string): number {
  return name.length;
};

// 类型标注
// (name: string) => number 表示函数的类型签名
const foo3: (name: string) => number = function (name: string): number {
  return name.length;
};

// 箭头函数的类型标注
const foo4: (name: string) => number = (name: string): number => {
  return name.length;
};

// 箭头函数的类型推断
const foo5 = (name: string): number => {
  return name.length;
};

为了提高可读性,一般要么直接在函数中进行参数和返回值的类型声明,要么使用类型别名将函数声明抽离出来。

代码语言:javascript
代码运行次数:0
运行
复制
type FuncFoo = (name: string) => number;

const foo: FuncFoo = (name) => {
  return name.length;
};

如果只是为了描述函数的类型结构,也可以使用 interface 进行函数声明:

代码语言:javascript
代码运行次数:0
运行
复制
interface FuncFooStruct {
  (name: string): number;
}

用来描述函数的 interface 被称为 Callable Interfaceinterface 用来描述一个类型结构,而函数类型本质上也是一个结构固定的类型。

# void 类型

在 TypeScript 中,一个没有返回值(即没有调用 return 语句)的函数,其返回值类型应该被标记为 void 而不是 undefined,尽管它的实际值就是 undefined

代码语言:javascript
代码运行次数:0
运行
复制
function foo(): void {
  console.log('foo');
}

function bar(): void {
  return;
}

在 TypeScript 中,undefined 类型是一个实际的、有意义的类型值,而 void 才代表空的、没有意义的类型值。

void 更强调没有返回,如果返回但是没有返回实际的值,推荐用 undefined

代码语言:javascript
代码运行次数:0
运行
复制
function bar(): undefined {
  return;
}

# 可选参数与 rest 参数

代码语言:javascript
代码运行次数:0
运行
复制
function foo1(name: string, age?: number): number {
  const inputAge = age || 18;
  return name.length + inputAge;
}

// 直接为可选参数指定默认值
// 既然有默认值,当然可以不传,所以不用强调可选
function foo2(name: string, age: number = 18): number {
  return name.length + age;
}

可选参数必须位于必选参数之后。

rest 参数实际上是一个数组,使用数组类型标注即可:

代码语言:javascript
代码运行次数:0
运行
复制
function foo(arg1: string, ...rest: any[]) {}

rest 参数也可以用元组类型进行标注:

代码语言:javascript
代码运行次数:0
运行
复制
function foo(arg1: string, ...rest: [number, boolean]) {}

foo('a', 1, true);

# 重载

要实现与入参关联的返回值类型,可以使用 TypeScript 提供的函数重载签名

代码语言:javascript
代码运行次数:0
运行
复制
function func(foo: number, bar: true): string;
function func(foo: number, bar?: false): number;
function func(foo: number, bar?: boolean): string | number {
  if (bar) {
    return String(foo);
  } else {
    return foo * 10;
  }
}

console.log(func(2022)); // 20220
console.log(func(2022, false)); // 20220
console.log(func(2022, true)); // '2022'

function func 的不同意义:

  • function func(foo: number, bar: true): string;,重载签名一,传入 bar 的值为 true 时,返回值类型为 string
  • function func(foo: number, bar?: false): number;,重载签名二,bar不传值或传入 bar 的值为 false 时,返回值类型为 number
  • function func(foo: number, bar?: boolean): string | number;,函数的实现签名,包含重载签名的所有可能情况

基于重载签名,实现了将入参类型和返回值类型的可能情况进行关联,获得了更精确的类型标注能力。

拥有多个重载声明的函数在被调用时,是按照重载的声明顺序往下查找的。

TypeScript 中的重载更像伪重载,只有一个具体的实现,其重载体现在方法调用的签名上而不是具体实现细节上。在像 C++ 等语言中,重载体现在多个名称一样,但是入参不同的函数实现上。

# 异步函数、Generator 函数 等类型签名

代码语言:javascript
代码运行次数:0
运行
复制
async function asyncFunc(): Promise<void> {}

function* genFunc(): Iterable<void>{}

async function* asyncGenFunc(): AsyncIterable<void>{}

// "use strict";
// async function asyncFunc() { }
// function* genFunc() { }
// async function* asyncGenFunc() { }

# Class

# 类与类成员的类型签名

类的主要结构有构造函数属性方法访问符。属性的类型标注类似于变量,构造函数、方法、存取器的类型标注类似于函数。

代码语言:javascript
代码运行次数:0
运行
复制
class Foo {
  prop: string;

  constructor(inputProp: string) {
    this.prop = inputProp;
  }

  print(addon: string): void {
    console.log(`${this.prop} ${addon}`);
  }

  get propA(): string {
    return `${this.prop}A`;
  }

  set propA(value: string) {
    this.prop = `${value}A`;
  }
}

注意,setter 方法不允许进行返回值的类型标注,因为其返回值并不会被消费,它更多地关注过程。

类也可以通过类声明类表达式的方法创建:

代码语言:javascript
代码运行次数:0
运行
复制
const Foo = class {
  prop: string;

  constructor(inputProp: string) {
    this.prop = inputProp;
  }

  print(addon: string): void {
    console.log(`${this.prop} ${addon}`);
  }
}

# 修饰符

在 TypeScript 中可以为 Class 成员添加修饰符,修饰符有:publicprivateprotectedreadonlyreadonly 属于操作性修饰符,其他的都是访问性修饰符。

代码语言:javascript
代码运行次数:0
运行
复制
class Foo {
  private prop: string;

  constructor(inputProp: string) {
    this.prop = inputProp;
  }

  protected print(addon: string): void {
    console.log(`${this.prop} ${addon}`);
  }

  public get propA(): string {
    return `${this.prop}A`;
  }

  public set propA(value: string) {
    this.prop = `${value}A`;
  }
}

通常不会为构造函数添加修饰符,而是让其保持默认的 public

各修饰符的含义:

  • public 此类成员在类、类的实例、子类中都可以访问;
  • private 此类成员只能在类的内部访问;
  • protected 此类成员只能在类的内部和子类中访问;

不显式使用访问性修饰符,默认会被标记为 public

为了简单,可以在构造函数中对参数应用访问性修饰符。参数会被直接作为类的成员(即实例的属性),不需要再手动添加属性和赋值。

代码语言:javascript
代码运行次数:0
运行
复制
class Foo {
  constructor(public arg1: string, private arg2: boolean) {}
}

const foo = new Foo('a', true);
console.log(foo.arg1); // 'a'

# 静态成员

代码语言:javascript
代码运行次数:0
运行
复制
class Foo {
  static staticHandler() {}
  public innerHandler() {}
}

在类的内部静态成员无法通过 this 来访问,需要通过 Foo.staticHandler() 的方式来访问。

代码语言:javascript
代码运行次数:0
运行
复制
"use strict";
var Foo = /** @class */ (function () {
    function Foo() {
    }
    Foo.staticHandler = function () { };
    Foo.prototype.innerHandler = function () { };
    return Foo;
}());

静态成员直接被挂载在函数体上,而实例成员被挂载在原型上。静态成员不会被实例继承,始终属于当前定义的这个类(及其子类)。原型对象上的实例成员会沿着原型链进行传递,能被继承。

# 继承、实现、抽象类

代码语言:javascript
代码运行次数:0
运行
复制
// 基类
class Base {}

// 派生类
class Derived extends Base {}

基类中哪些成员可以被派生类访问,由其访问性修饰符决定。派生类可以访问使用 publicprotected 修饰符的基类成员。除了访问外,派生类可以覆盖基类中的方法,但仍然可以通过 super 来调用基类的方法。

代码语言:javascript
代码运行次数:0
运行
复制
class Base {
  print() {}
}

class Derived extends Base {
  print() {
    super.print();
  }
}

为了检查被覆盖的基类方法在基类中确实存在,可以使用 override 关键字:

代码语言:javascript
代码运行次数:0
运行
复制
class Base {
  print() {}
}

class Derived extends Base {
  override print() {
    super.print();
  }
}

抽象类 是对类结构与方法的抽象,抽象类描述一个类中有哪些成员(属性,方法等),抽象方法描述这一个方法在实际实现中的结构。

代码语言:javascript
代码运行次数:0
运行
复制
abstract class AbsFoo {
  abstract absProp: string;
  abstract get absGetter(): string;
  abstract absMethod(name: string): string;
}

抽象类中的成员需要使用 abstract 关键字才能被视为抽象类成员。

代码语言:javascript
代码运行次数:0
运行
复制
class Foo implements AbsFoo {
  absProp: string;

  get absGetter() {
    return this.absProp;
  }

  absMethod(name: string) {
    return `${name} ${this.absProp}`;
  }
}

必须完全实现抽象类中的每一个成员。在 TypeScript 中无法声明静态的抽象成员

对于抽象类,其本质是描述类的结构,因此也可以用 interface 来声明类的结构。

代码语言:javascript
代码运行次数:0
运行
复制
interface FooStruct {
  absProp: string;
  get absGetter(): string;
  absMethod(name: string): string;
}

class Foo implements FooStruct {
  absProp: string;

  get absGetter() {
    return this.absProp;
  }

  absMethod(name: string) {
    return `${name} ${this.absProp}`;
  }
}

# 私有构造函数

类的构造函数被标记为私有,只允许在类内部访问,无法实例化。此时,可以使用私有构造函数来组织其被错误的实例化,如在创建 Utils 类时,其内部都是静态成员。

代码语言:javascript
代码运行次数:0
运行
复制
class Utils {
  public static identifier = 'Cell';

  private constructor() {}

  public static getIdentifier() {
    return Utils.identifier;
  }
}

或者在一个类希望把实例化逻辑通过方法来实现,而不是通过 new 实现,可以使用私有构造函数。

代码语言:javascript
代码运行次数:0
运行
复制
class Foo {
  private constructor() {}

  public static create() {
    return new Foo();
  }
}

const foo = Foo.create();

# SOLID 原则

  • S 单一功能原则,一个类应该仅有一种职责
  • O 开放封闭原则,一个类应该对扩展开放,对修改封闭
  • L 里氏替换原则,派生类可以在程序的任何移除对其基类进行替换,即子类完全继承父类的一切,只是对其功能进行扩展
  • I 接口隔离原则,类的实现方法应该只需要实现自己需要的那部分接口,而不是实现所有接口
  • D 依赖倒置原则,高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象

# 内置类型

# any

为了能够表示“任意类型”,TypeScript 提供了一个内置类型 any,用来表示任意类型。

代码语言:javascript
代码运行次数:0
运行
复制
log(message?: any, ...optionalParams?: any[]): void;

除了显式标记一个变量或参数为 any,在某些情况下一些变量或参数会被隐式推导为 any 类型,如:

代码语言:javascript
代码运行次数:0
运行
复制
let foo;

function func(foo, bar){} // foo, bar 都会被推导为 any 类型

any 类型的变量几乎无所不能,它可以在声明后再次接受任意类型的值,同时可以被赋值给任意其他类型的变量:

代码语言:javascript
代码运行次数:0
运行
复制
let anyVal: any = 'Cell';

anyVal = 123;
anyVal = true;
anyVal = null;

anyVal = () => {};

let strVal: string = anyVal;
let numVal: number = anyVal;

可以在 any 类型变量上任意地进行操作,包括赋值、访问、方法调用等,此时可以认为类型推导与检查时完全被禁用的:

代码语言:javascript
代码运行次数:0
运行
复制
let anyVal: any = null;

anyVal.foo.bar();
anyVal();
anyVal = 'Cell';

any 类型的主要意义,是为了表示一个无拘无束的“任意类型”,它能兼容所有类型,也能被所有类型兼容

any 的本质是类型系统中的顶级类型。

any 类型的万能性会导致其被经常滥用,需要注意:

  • 如果是类型不兼容报错导致要使用 any,考虑使用类型断言代替
  • 如果是类型太复杂导致不想全部声明要使用 any, 考虑去将这里的类型去断言为需要的最简类型
  • 如果是要表达一个未知类型,考虑使用 unknown 类型

# unknown

unknown 类型的变量可以再次赋值为任意其他类型,但注意只能赋值给 anyunknown 类型的变量:

代码语言:javascript
代码运行次数:0
运行
复制
let unknownVal: unknown = 'Cell';

unknownVal = 123;
unknownVal = true;
unknownVal = null;

unknownVal = () => {};

let anyVal: any = unknownVal;
let unknownVal2: unknown = unknownVal;
let strVal: string = unknownVal; // Error

unknownany 的主要差异体现在赋值给别的变量时,any 把所有类型都兼容,而 unknown期待一个确定的值

any 放弃了所有的类型检查,而 unknown 没有:

代码语言:javascript
代码运行次数:0
运行
复制
let unknownVal: unknown;

unknownVal.foo(); // Error

要对 unknown 类型进行属性访问,需要进行类型断言,即许诺它会是一个确定的类型:

代码语言:javascript
代码运行次数:0
运行
复制
let unknownVal: unknown;

(unknownVal as { foo: () => {}}).foo();

# never

never 是一个“什么都没有”的类型,不携带任何的类型信息。

never 类型被称为 Bottom Type,是整个类型系统层级中最底层的类型。

nullundefined 一样,是所有类型的子类型,但只有 never 类型的变量可以赋值给另一个 never 类型的变量。

通常不会显式声明一个 never 类型,它主要被类型检查所使用。

# 类型断言

类型断言可以显式告知类型检查程序当前变量的类型。是一个将变量的已有类型更改为新指定的类型的操作。

代码语言:javascript
代码运行次数:0
运行
复制
let unknownVar: unknown;

(unknownVar as { foo: () => {} }).foo();

类型断言正确使用方式是,在 TypeScript 类型分析不正确或不符合预期时,将其断言为此处的正确类型:

代码语言:javascript
代码运行次数:0
运行
复制
interface IFoo {
  name: string;
}

declare const obj: {
  foo: IFoo;
};

const {
  foo = {} as IFoo,
} = obj;

# 双重断言

在原类型与断言类型之间差异过大时,需要先断言到一个通用的类型,anyunknown,再进行第二次断言:

代码语言:javascript
代码运行次数:0
运行
复制
const str: string = 'Cell';

(str as unknown as { handler: () => {} }).handler();

// 尖括号语法
(<{ handler: () => {} }>(<unknown>str)).handler();

# 非空断言

非空断言是类型断言的简化,标记前面的一个声明一定是非空的,即剔除 nullundefined 类型:

代码语言:javascript
代码运行次数:0
运行
复制
declare const foo: {
  func?: () => ({
    prop?: number | null
  })
}

foo.func!().prop!;
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022/8/1,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • # 学习方法
    • # 最佳学习路径
    • # VS Code 插件
    • # 快速练习
    • # TS 文件快速执行
  • # 原型类型和对象类型
    • # 原始类型标注
    • # null 和 undefined
    • # void
  • # 数组类型标注
    • # 元组
  • # 对象类型标注
    • # 修饰接口属性
    • # type 与 interface
    • # object、Object 和 {}
    • # Symbol
  • # 字面量类型
    • # 字面量类型和联合类型
    • # 字面量类型
    • # 联合类型
    • # 对象字面量类型
  • # 枚举
    • # 常量枚举
  • # 函数
    • # 函数的类型签名
    • # void 类型
    • # 可选参数与 rest 参数
    • # 重载
    • # 异步函数、Generator 函数 等类型签名
  • # Class
    • # 类与类成员的类型签名
    • # 修饰符
    • # 静态成员
    • # 继承、实现、抽象类
    • # 私有构造函数
    • # SOLID 原则
  • # 内置类型
    • # any
    • # unknown
    • # never
  • # 类型断言
    • # 双重断言
    • # 非空断言
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档