ts相关笔记(基础必看)

推荐一下小册 TypeScript 全面进阶指南,此篇笔记来源于此,记录总结,加深印象!

另外,如果想了解更多ts相关知识,可以参考我的其他笔记:

  • vue3+ts开发干货笔记
  • TSConfig 配置(tsconfig.json)
  • ts相关笔记(Partial、Required、Readonly、Record、Exclude、Extract)
  • ts相关笔记(类型层级)
  • ts相关笔记(extends、infer、Pick、Omit)

原始类型和对象类型

原始类型

除了最常见的 number / string / boolean / null / undefined, ES6、ES11)又分别引入了 2 个新的原始类型:symbol 与 bigint

const name: string = 'ts';
const age: number = 18;
const male: boolean = false;
const undef: undefined = undefined;
const nul: null = null;
const obj: object = { name, age, male };
const bigintVar1: bigint = 90071992547409212n;
const bigintVar2: bigint = BigInt(9007199254740991);
const symbolVar: symbol = Symbol('unique');

null和undefined

  • 在js里,null表示一个空值,undefined表示没有值
  • 在ts里,null 与 undefined 类型都是有具体意义的类型

在没有开启strictNullChecks 检查的情况下,这两种类型会被视作其他类型的子类型,比如 string 类型会被认为包含了 null 与 undefined 类型:

const tmp1: null = null;
const tmp2: undefined = undefined;

const tmp3: string = null; // 仅在关闭 strictNullChecks 时成立,下同
const tmp4: string = undefined;

void

用于描述一个内部没有 return 语句,或者没有显式 return 一个值的函数的返回值,如:

function func1() {}
function func2() {
  return;
}
function func3() {
  return undefined;
}

数组类型标注

const arr1: string[] = []; // 更推荐此写法

const arr2: Array<string> = [];
元组(Tuple)

元组就是类型和数据的个数一开始就已经限定好了,某些情况下使用元组代替数组要更加妥当。
下面是一些应用

  • 提供越界提醒

    const arr3: string[] = ['aaa', 'bbb', 'ccc']
    const arr4: [string,string,string] = ['aaa', 'bbb', 'ccc']
    console.log(arr4[4])
    

    在这里插入图片描述

  • 可选

    const arr6: [string, number?, boolean?] = ['aaa'];
    // 下面这么写也可以
    // const arr6: [string, number?, boolean?] = ['aaa', , ,];
    
  • 具名元组
    在 TypeScript 4.0 中,有了具名元组(Labeled Tuple Elements)的支持,使得我们可以为元组中的元素打上类似属性的标记:

    const arr7: [name: string, age: number, male?: boolean] = ['aaa', 18, true];
    

对象类型标注

使用interface接口
interface IDescription {
  name: string;
  age: number;
  male: boolean;
}

const obj1: IDescription = {
  name: 'aaa',
  age: 599,
  male: true,
};

可选 ?

interface IDescription {
  name: string;
  age: number;
  male?: boolean;
  func?: Function;
}

const obj2: IDescription = {
  name: 'aaa',
  age: 599,
  male: true,
  // 无需实现 func 也是合法的
};

只读

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

const obj3: IDescription = {
  name: 'aaa',
  age: 599,
};

// 无法分配到 "name" ,因为它是只读属性
obj3.name = "AAA";
使用type 类型别名
type User = {
    name: string
    age: number
};

很多人更喜欢用 type(Type Alias,类型别名)来代替接口结构描述对象,
而更推荐的方式是

  • interface 用来描述对象、类的结构
  • type类型别名用来将一个函数签名、一组联合类型、一个工具类型等等抽离成一个完整独立的类型

但大部分场景下接口结构都可以被类型别名所取代,取决于个人喜好或者团队的一些规范吧。

object、Object 以及 { }

Object

js中,原型链的顶端是 Object 以及 Function,这也就意味着所有的原始类型与对象类型最终都指向 Object,在 TypeScript 中就表现为 Object 包含了所有的类型

// 对于 undefined、null、void 0 ,需要关闭 strictNullChecks
const tmp1: Object = undefined;
const tmp2: Object = null;
const tmp3: Object = void 0;

const tmp4: Object = 'aaa';
const tmp5: Object = 599;
const tmp6: Object = { name: 'aaa' };
const tmp7: Object = () => {};
const tmp8: Object = [];

和 Object 类似的还有 Boolean、Number、String、Symbol,这几个装箱类型(Boxed Types) 同样包含了一些超出预期的类型。
以 String 为例,它同样包括 undefined、null、void,以及代表的 拆箱类型(Unboxed Types) —> string,但并不包括其他装箱类型对应的拆箱类型,如 boolean 与 基本对象类型,我们看以下的代码:

const tmp9: String = undefined;
const tmp10: String = null;
const tmp11: String = void 0;
const tmp12: String = 'aaa';

// !!!  以下不成立,因为不是字符串类型的拆箱类型
const tmp13: String = 599; 
const tmp14: String = { name: 'aaa' }; 
const tmp15: String = () => {}; 
const tmp16: String = []; 

object
object 的引入就是为了解决对 Object 类型的错误使用,它代表所有非原始类型的类型,即数组、对象与函数类型这些:

const tmp17: object = undefined;
const tmp18: object = null;
const tmp19: object = void 0;

const tmp20: object = 'aaa';  // X 不成立,值为原始类型
const tmp21: object = 599; // X 不成立,值为原始类型

const tmp22: object = { name: 'aaa' };
const tmp23: object = () => {};
const tmp24: object = [];

{ }
可以认为使用{ }作为类型签名就是一个合法的,但内部无属性定义的空对象,
这类似于 Object(想想 new Object()),它意味着任何非 null / undefined 的值:

const tmp25: {} = undefined; // 仅在关闭 strictNullChecks 时成立,下同
const tmp26: {} = null;
const tmp27: {} = void 0; // void 0 等价于 undefined

const tmp28: {} = 'aaa';
const tmp29: {} = 599;
const tmp30: {} = { name: 'aaa' };
const tmp31: {} = () => {};
const tmp32: {} = [];

虽然能够将其作为变量的类型,但你实际上无法对这个变量进行任何赋值操作:

const tmp30: {} = { name: 'aaa' };

tmp30.age = 18; // X 类型“{}”上不存在属性“age”。

总结

  • 在任何时候都不要,不要,不要使用 Object 以及类似的装箱类型。

  • 当你不确定某个变量的具体类型,但能确定它不是原始类型,可以使用
    object。但更推荐进一步区分,也就是使用
    Record 或 Record 表示对象,
    unknown[] 或 any[] 表示数组,
    (…args: any[]) => any表示函数 这样。

  • 我们同样要避免使用{}。{}意味着任何非 null / undefined 的值,从这个层面上看,使用它和使用 any 一样恶劣。

字面量类型和枚举

字面量类型

看一个例子,我们开发中定义的请求 接口返回 结果可能如下

interface Res {
  code: 10000 | 10001 | 50000;
  status: "success" | "failure";
  data: any;
}

上面"success" 或者 “failure” 不是一个值吗?为什么它也可以作为类型?

在 TypeScript 中,这叫做字面量类型(Literal Types),它代表着比原始类型更精确的类型,同时也是原始类型的子类型

字面量类型主要包括:字符串字面量类型、数字字面量类型、布尔字面量类型和对象字面量类型,它们可以直接作为类型标注:

const str: "aaa" = "aaa";
const num: 599 = 599;
const bool: true = true
// 报错!Type '"aaa123"' is not assignable to type '"aaa"'
const str1: "aaa" = "aaa123";

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

interface Tmp {
  bool: true | false;
  num: 1 | 2 | 3;
  str: "aa" | "bb" | "cc"
}

补充:联合类型

联合类型你可以理解为,它代表了一组类型的可用集合,只要最终赋值的类型属于联合类型的成员之一,就可以认为符合这个联合类型。
联合类型对其成员并没有任何限制,除了上面这样对同一类型字面量的联合,我们还可以将各种类型混合到一起:

interface Tmp {
  mixed: true | string | 599 | {} | (() => {}) | (1 | 2)
}

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

interface Tmp {
  user:
    | {
        vip: true;
        expires: string;
      }
    | {
        vip: false;
        promotion: string;
      };
}

declare var tmp: Tmp;

if (tmp.user.vip) {
  console.log(tmp.user.expires);
}
枚举
// js中定义一些常量
export const PageUrl = {
  Home_Page_Url: "url1",
  Setting_Page_Url: "url2",
  Share_Page_Url: "url3",
}

使用ts的枚举 enum

enum PageUrl {
  Home_Page_Url = "url1",
  Setting_Page_Url = "url2",
  Share_Page_Url = "url3",
}
// 使用
const home = PageUrl.Home_Page_Url;

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

enum Items {
  Foo,
  Bar,
  Baz
}
// Items.Foo , Items.Bar , Items.Baz的值依次是 0,1,2 

如果你只为某一个成员指定了枚举值,那么之前未赋值成员仍然会使用从 0 递增的方式,之后的成员则会开始从枚举值递增。

enum Items {
  Foo,  // 0 
  Bar = 599,
  Baz // 600
}

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

enum Items {
  Foo,
  Bar,
  Baz
}

const fooValue = Items.Foo; // 0
const fooKey = Items[0]; // "Foo"

函数和class中的类型

函数
// 函数声明
function foo(name: string): number {
  return name.length;
}
// 函数表达式

const foo = function (name: string): number {
  return name.length
}
const foo: (name: string) => number = function (name) {
  return name.length
}

// 箭头函数
// 方式一
const foo = (name: string): number => {
  return name.length
}

// 方式二 代码的可读性会非常差,不推荐
const foo: (name: string) => number = (name) => {
  return name.length
}

要么直接在函数中进行参数和返回值的类型声明,要么使用类型别名将函数声明抽离出来,如下:

type FuncFoo = (name: string) => number

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

如果只是为了描述这个函数的类型结构,我们甚至可以使用 interface 来进行函数声明:

interface FuncFooStruct {
  (name: string): number
}

void 类型

// 没有调用 return 语句
function foo(): void { }

// 调用了 return 语句,但没有返回值
function bar(): void {
  return;
}

可选参数与 rest 参数

使用 ? 描述一个可选参数
注意:可选参数必须位于必选参数之后

// 在函数逻辑中注入可选参数默认值
function foo1(name: string, age?: number): number {
  const inputAge = age || 18; // 或使用 age ?? 18
  return name.length + inputAge
}

// 直接为可选参数声明默认值
function foo2(name: string, age: number = 18): number {
  const inputAge = age;
  return name.length + inputAge
}

对于 rest 参数的类型标注也比较简单,由于其实际上是一个数组,这里我们也应当使用数组类型进行标注:

function foo(arg1: string, ...rest: any[]) { }

// 元组标注
function foo(arg1: string, ...rest: [number, boolean]) { }

foo("aaa", 18, true)

重载
在某些逻辑较复杂的情况下,函数可能有多组入参类型和返回值类型:

function func(foo: number, bar?: boolean): string | number {
  if (bar) {
    return String(foo);
  } else {
    return foo * 599;
  }
}

要想实现与入参关联的返回值类型,我们可以使用 TypeScript 提供的函数重载签名(Overload Signature),将以上的例子使用重载改写:

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 * 599;
  }
}

const res1 = func(599); // number
const res2 = func(599, true); // string
const res3 = func(599, false); // number

异步函数

async function asyncFunc(): Promise<void> {}

对于异步函数(即标记为 async 的函数),其返回值必定为一个 Promise 类型,而 Promise 内部包含的类型则通过泛型的形式书写,即 Promise < T >

class

一个函数的主要结构即是参数、逻辑和返回值,对于逻辑的类型标注其实就是对普通代码的标注,所以我们只介绍了对参数以及返回值的类型标注。

而到了 Class 中其实也一样,它的主要结构只有构造函数、属性、方法和访问符(Accessor),我们也只需要关注这三个部分即可

构造函数

class Foo {
  prop: string;

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

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

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

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

!!setter 方法不允许进行返回值的类型标注,你可以理解为 setter 的返回值并不会被消费,它是一个只关注过程的函数。

修饰符
分别有 public / private / protected / readonly
readonly 属于操作性修饰符(就和 interface 中的 readonly 意义一致)
其它三个属于访问性修饰符

  • public:此类成员在类、类的实例、子类中都能被访问。
  • private:此类成员仅能在类的内部被访问。
  • protected:此类成员仅能在类与子类中被访问,你可以将类和类的实例当成两种概念,即一旦实例化完毕(出厂零件),那就和类(工厂)没关系了,即不允许再访问受保护的成员。
class Foo {
  private prop: string;

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

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

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

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

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

abstract class AbsFoo {
  abstract absProp: string;
  abstract get absGetter(): string;
  abstract absMethod(name: string): string
}

抽象类中的成员也需要使用 abstract 关键字才能被视为抽象类成员,如这里的抽象方法。我们可以实现(implements)一个抽象类:

class Foo implements AbsFoo {
  absProp: string = "aaa"

  get absGetter() {
    return "aaa"
  }

  absMethod(name: string) {
    return name
  }
}

另外使用 interface 不仅可以声明函数结构,也可以声明类的结构:

interface FooStruct {
  absProp: string;
  get absGetter(): string;
  absMethod(input: string): string
}

class Foo implements FooStruct {
  absProp: string = "aaa"

  get absGetter() {
    return "aaa"
  }

  absMethod(name: string) {
    return name
  }
}

any、unknown、never

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

千万不要 anyscript !!!

unknown 类型的变量可以再次赋值为任意其它类型,但只能赋值给 any 与 unknown 类型的变量

let unknownVar: unknown = "aaa";

unknownVar = false;
unknownVar = "aaa";
unknownVar = {
  site: "bbb"
};

unknownVar = () => { }

const val1: string = unknownVar; // Error
const val2: number = unknownVar; // Error
const val3: () => {} = unknownVar; // Error
const val4: {} = unknownVar; // Error

const val5: any = unknownVar;
const val6: unknown = unknownVar;

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

let unknownVar: unknown;

unknownVar.foo(); // 报错:对象类型为 unknown
// 类型断言
let unknownVar: unknown;

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

类型断言: 虽然这是一个未知的类型,但我跟你保证它在这里就是这个类型!

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

never 类型不携带任何的类型信息,因此会在联合类型中被直接移除
ts相关笔记(基础必看)_第1张图片

泛型

可以理解为一个接受参数的函数

类型别名type中的泛型

type Factory<T> = T | number | string;

type中的泛型大多是用来进行工具类型封装,比如

// 把一个对象中的所有属性变成 string类型
type Stringify<T> = {
  [K in keyof T]: string;
};

// 复制对象中的所有属性类型
type Clone<T> = {
  [K in keyof T]: T[K];
};

看一个例子

type Partial<T> = {
    [P in keyof T]?: T[P];
};
interface IFoo {
  prop1: string;
  prop2: number;
  prop3: boolean;
  prop4: () => void;
}

type PartialIFoo = Partial<IFoo>;


// 等价于
interface PartialIFoo {
  prop1?: string;
  prop2?: number;
  prop3?: boolean;
  prop4?: () => void;
}

泛型还可以作为条件类型中的判断条件

type IsEqual<T> = T extends true ? 1 : 2;

type A = IsEqual<true>; // 1
type B = IsEqual<false>; // 2
type C = IsEqual<'linbudu'>; // 2

可以设置默认值

  • List item
type Factory<T = boolean> = T | number | string;

// 调用时就可以不带任何参数了,默认会使用我们声明的默认值来填充
const foo: Factory = false;

可以使用 extends 关键字来约束传入的泛型参数必须符合要求
比如:

  • A extends B 意味着 A 是 B 的子类型
  • ‘aaa’ extends string,18 extends number 成立。
  • 联合类型子集均为联合类型的子类型,即 1、 1 | 2 是 1 | 2 | 3 | 4 的子类型
  • { name: string } 是 {} 的子类型,因为在 {} 的基础上增加了额外的类型

看下面例子,根据传入的请求码判断请求是否成功

type ResStatus<ResCode extends number> = ResCode extends 10000 | 10001 | 10002
  ? 'success'
  : 'failure';

type Res1 = ResStatus<10000>; // "success"
type Res2 = ResStatus<20000>; // "failure"

type Res3 = ResStatus<'10000'>; // 类型“string”不满足约束“number”。
type ResStatus<ResCode extends number = 10000> = ResCode extends 10000 | 10001 | 10002
  ? 'success'
  : 'failure';

type Res4 = ResStatus; // "success"

多泛型关联
不仅可以同时传入多个泛型参数,还可以让这几个泛型参数之间也存在联系,类似于传入多个参数

type Conditional<Type, Condition, TruthyResult, FalsyResult> =
  Type extends Condition ? TruthyResult : FalsyResult;

//  "passed!"
type Result1 = Conditional<'aaa', string, 'passed!', 'rejected!'>;

// "rejected!"
type Result2 = Conditional<'bbb', boolean, 'passed!', 'rejected!'>;

接口interface中的泛型

常见的一个例子应该还是响应类型结构的泛型处理:

interface IRes<TData = unknown> {
  code: number;
  error?: string;
  data: TData;
}

这个接口描述了一个通用的响应类型结构,预留出了实际响应数据的泛型坑位,然后在你的请求函数中就可以传入特定的响应类型了:

interface IUserProfileRes {
  name: string;
  homepage: string;
  avatar: string;
}

function fetchUserProfile(): Promise<IRes<IUserProfileRes>> {}

type StatusSucceed = boolean;
function handleOperation(): Promise<IRes<StatusSucceed>> {}

函数中的泛型

函数中的泛型是很常用的,主要是做:类型的自动提取

function handle<T>(input: T): T {}

我们为函数声明了一个泛型参数 T,并将参数的类型与返回值类型指向这个泛型参数。这样,在这个函数接收到参数时,T 会自动地被填充为这个参数的类型。

例子:

function handle<T>(input: T): T {}

const author = "aaa"; // 使用 const 声明,被推导为 "aaa"

let authorAge = 18; // 使用 let 声明,被推导为 number

handle(author); // 填充为字面量类型 "aaa"
handle(authorAge); // 填充为基础类型 number

在基于参数类型进行填充泛型时,其类型信息会被推断到尽可能精确的程度,如这里会推导到字面量类型而不是基础类型。
这是因为在直接传入一个值时,这个值是不会再被修改的,因此可以推导到最精确的程度。而如果你使用一个变量作为参数,那么只会使用这个变量标注的类型(在没有标注时,会使用推导出的类型)。

例子:

function swap<T, U>([start, end]: [T, U]): [U, T] {
  return [end, start];
}

const swapped1 = swap(["aaa", 18]); // const swapped1: [number, string]
const swapped2 = swap([null, 18]); // const swapped2: [number, null]
const swapped3 = swap([{ name: "aaa" }, {}]); // const swapped3: [{}, { name: string;}]

函数中的泛型同样存在约束与默认值

// 不再处理对象类型的情况了
function handle<T extends string | number>(input: T): T {}
// 只想处理数字元组的情况
function swap<T extends number, U extends number>([start, end]: [T, U]): [U, T] {
  return [end, start];
}

函数的泛型参数也会被内部的逻辑消费,如:

function handle<T>(payload: T): Promise<[T]> {
  return new Promise<[T]>((res, rej) => {
    res([payload]);
  });

对于箭头函数的泛型,其书写方式是这样的:

const handle = <T>(input: T): T => {}

在 tsx 文件中泛型的尖括号可能会造成报错,编译器无法识别这是一个组件还是一个泛型,此时你可以让它长得更像泛型一些:

const handle = <T extends any>(input: T): T => {}

需要注意的是,不要为了用泛型而用泛型,就像这样:
没有意义

function handle<T>(arg: T): void {
  console.log(arg);
};

Class 中的泛型

Class 中的泛型和函数中的泛型非常类似,只不过函数中泛型参数的消费方是参数和返回值类型
Class 中的泛型消费方则是属性、方法、乃至装饰器等。
同时 Class 内的方法还可以再声明自己独有的泛型参数。我们直接来看完整的示例:

class Queue<TElementType> {
  private _list: TElementType[];

  constructor(initial: TElementType[]) {
    this._list = initial;
  }

  // 入队一个队列泛型子类型的元素
  enqueue<TType extends TElementType>(ele: TType): TElementType[] {
    this._list.push(ele);
    return this._list;
  }

  // 入队一个任意类型元素(无需为队列泛型子类型)
  enqueueWithUnknownType<TType>(element: TType): (TElementType | TType)[] {
    return [...this._list, element];
  }

  // 出队
  dequeue(): TElementType[] {
    this._list.shift();
    return this._list;
  }
}

你可能感兴趣的:(typescript笔记,笔记,typescript)