TypeScript学习笔记

155 阅读14分钟

Day1

插件

  • TypeScript Importer:收集你项目内所有的类型定义,在你敲出:时提供这些类型来进行补全。如果你选择了一个,它还会自动帮你把这个类型导入进来
  • Move TS:通过编辑文件的路径,直接修改项目的目录结构
  • Error Lens:把你的 VS Code 底部问题栏的错误下直接显示到代码文件中的对应位置

TS文件的快速执行

  • ts-node
npm i ts-node typescript -g
  • ts-node-dev
    • 支持自动地监听文件变更然后重新执行
    • ts-node-dev 基于 node-dev(你可以理解一个类似 nodemon 的库,提供监听文件重新执行的能力) 与 ts-node 实现,并在重启文件进程时共享同一个 TS 编译进程,避免了每次重启时需要重新实例化编译进程等操作。
npm i ts-node-dev -g

null和undefined

  • 在 TypeScript 中,nullundefined 类型都是有具体意义的类型
  • 在没有开启 strictNullChecks 检查的情况下,会被视作其他类型的子类型,比如 string 类型会被认为包含了 nullundefined 类型
  • void 表示一个空类型,而 nullundefined 都是一个具有意义的实际类型

元组(Tuple)

  • 具名元组
const arr: [name: string, age: number, male: boolean] = ['Kylin', 18, true];

type 与 interface

  • type(Type Alias,类型别名):将一个函数签名、一组联合类型、一个工具类型等等抽离成一个完整独立的类型。

  • interface 用来描述对象、类的结构

  • 但大部分场景下接口结构都可以被类型别名所取代,因此,只要你觉得统一使用类型别名让你觉得更整齐,也没什么问题。

object、Object 以及 { }

  • object 的引入就是为了解决对 Object 类型的错误使用,它代表所有非原始类型的类型,即数组、对象与函数类型
const tmp17: object = undefined;
const tmp18: object = null;
const tmp19: object = void 0;

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

const tmp22: object = { name: 'Kylin' };
const tmp23: object = () => {};
const tmp24: object = [];
  • Object是装箱类型,原型链的顶端是 Object 以及 Function,这也就意味着所有的原始类型与对象类型最终都指向 Object,在 TypeScript 中就表现为 Object 包含了所有的类型。但不应该使用它

    • Object 类似的装箱类型还有 BooleanNumberStringSymbol
  • {}代表对象字面量类型,或者叫内部无属性定义的空对象,可以表示任何非 null / undefined 的值,不应该使用它。

    • 虽然能够将其作为变量的类型,但你实际上无法对这个变量进行任何赋值操作
    const tmp30: {} = { name: 'Kylin' };
    
    tmp30.age = 18; // X 类型“{}”上不存在属性“age”。
    

为了更好地区分 Objectobject 以及{}这三个具有迷惑性的类型,我们再做下总结:

  • 在任何时候都不要,不要,不要使用 Object 以及类似的装箱类型。
  • 当你不确定某个变量的具体类型,但能确定它不是原始类型,可以使用 object。但我更推荐进一步区分,也就是使用 Record<string, unknown>Record<string, any> 表示对象,unknown[]any[] 表示数组,(...args: any[]) => any表示函数这样。
  • 我们同样要避免使用{}{}意味着任何非 null / undefined 的值,从这个层面上看,使用它和使用 any 一样恶劣。

字面量类型

  • 它代表着比原始类型更精确的类型,同时也是原始类型的子类型
    • 原始类型的值可以包括任意的同类型值,而字面量类型要求的是值级别的字面量一致
// 字面量类型
const str: "Kylin" = "Kylin";
const num: 599 = 599;
const bool: true = true;

// 报错!不能将类型“"Kylin599"”分配给类型“"Kylin"”。
const str1: "Kylin" = "Kylin599";

const str2: string = "Kylin";
const str3: string = "Kylin599";
  • 字面量类型主要包括字符串字面量类型string数字字面量类型number布尔字面量类型boolean对象字面量类型object,它们可以直接作为类型标注

  • 通常与联合类型一起使用:|

    • 对于联合类型中的函数类型,需要使用括号()包裹起来
    • 函数类型并不存在字面量类型,因此这里的 (() => {}) 就是一个合法的函数类型
interface Tmp {
  bool: true | false;
  num: 1 | 2 | 3;
  str: "aaa" | "bbb" | "ccc"
}

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);
}

枚举

  • 数字枚举、字符串枚举;普通枚举、常量枚举
  • 枚举和对象的重要差异在于,对象是单向映射的,我们只能从键映射到键值。而枚举是双向映射的,即你可以从枚举成员映射到枚举值,也可以从枚举值映射到枚举成员:
enum Items {
  Foo,
  Bar,
  Baz
}

const fooValue = Items.Foo; // 0
const fooKey = Items[0]; // "Foo"
  • 仅有值为数字的枚举成员才能够进行这样的双向枚举,字符串枚举成员仍然只会进行单次映射

  • 默认情况下是数字枚举,且值为依次递增

enum Items {
  Foo, // 0
  Bar, // 1
  Baz // 2
}

Day2

函数

函数类型签名

  • 描述了函数入参类型与函数返回值类型
  • 要么直接在函数中进行参数和返回值的类型声明,要么使用类型别名将函数声明抽离出来
const foo = function (name: string): number {
  return name.length
}
​
type FuncFoo = (name: string) => numberconst foo: FuncFoo = (name) => {
  return name.length
}

重载

  • 在某些逻辑较复杂的情况下,函数可能有多组入参类型和返回值类型:
function func(foo: number, bar?: boolean): string | number {
  if (bar) {
    return String(foo);
  } else {
    return foo * 599;
  }
}
​
// 重载签名1
function func(foo: number, bar: true): string;
// 重载签名2
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
  • TypeScript 中的重载更像是伪重载:

    • 它只有一个具体实现,其重载体现在方法调用的签名上而非具体实现上
    • 而在如 C++ 等语言中,重载体现在多个名称一致但入参不同的函数实现上

class

  • 主要结构只有

    • 构造函数
    • 属性
    • 方法
    • 访问符(Accessor)

修饰符

  • public:访问性修饰符,此类成员在类、类的实例、子类中都能被访问。
  • private:访问性修饰符,此类成员仅能在类的内部被访问。
  • protected:访问性修饰符,此类成员仅能在类与子类中被访问
  • readonly:操作性修饰

关键字

static:静态成员

  • 类的内部静态成员无法通过 this 来访问,需要通过 Foo.staticHandler 这种形式进行访问
class Foo {
  static staticHandler() { }
​
  public instanceHandler() { }
}

override

  • 确保派生类尝试覆盖的方法一定在基类中存在定义,如下会报错,在基类中未声明print
class Base {
  printWithLove() { }
}
​
class Derived extends Base {
  override print() {
    // ...
  }
}

class的类型

  • 基类:class Base { }
  • 派生类:class Derived extends Base { }
  • 抽象类:abstract class Abstract{ }描述了一个类中应当有哪些成员(属性、方法等)
abstract class AbsFoo {
  abstract absProp: string;
  abstract get absGetter(): string;
  abstract absMethod(name: string): string
}
  • 实现一个抽象类implements

    • 必须完全实现这个抽象类的每一个抽象成员
class Foo implements AbsFoo {
  get absGetter() {
    return "linbudu"
  }
​
  absMethod(name: string) {
    return name
  }
}
const foo = new Foo()
foo.absProp = 'foo' // 报错

image-20231219114716447

  • interface 不仅可以声明函数结构,也可以声明类的结构:

    • interface 只是指定检查条件,如果不满足这些条件就会报错。它并不能代替 class 自身的类型声明。
interface A {
  get(name:string): boolean;
}
​
class B implements A {
  get(s) { // s 的类型是 any
    return true;
  }
}
  • 上面示例中,类B实现了接口A,但是后者并不能代替B的类型声明。因此,Bget()方法的参数s的类型是any,而不是stringB类依然需要声明参数s的类型。
class B implements A {
  get(s:string) {
    return true;
  }
}

image-20231219115242014

Day3

Top Type

  • any

    • 会跳过类型检查器对值的检查,任何值都可以赋值给any类型, any类型的值也可以赋值给任何类型
    • 可以给一个any类型的变量赋值任何的值,比如数字、字符串的值;
     const arr: any[] = ["111", 234] //不推荐
    
  • unknown

    • 它用于描述类型不确定的变量
    • 任何类型的值都可以赋值给unknown类型,但unknow类型只能赋值给any和unknown类型
    • any类型可以赋值给任意类型
 let notSure: unknown = 4;
 let uncertain: any = notSure; // OK
 ​
 let notSure: any = 4;
 let uncertain: unknown = notSure; // OK
 ​
 let notSure: unknown = 4;
 let uncertain: number = notSure; // Error

Bottom Type

  • never

    • 整个类型系统层级中最底层的类型
    • 和 null、undefined 一样,它是所有类型的子类型,但只有 never 类型的变量能够赋值给另一个 never 类型变量。
    • 表示那些永不存在的值的类型
    • 函数中是一个死循环或者抛出一个异常
 function justThrow(): never {
   throw new Error()
 }

类型断言:警告编译器不准报错

  • 通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。

    • 在 TypeScript 类型分析不正确或不符合预期时,将其断言为此处的正确类型
  • 类型断言好比其它语言里的类型转换

  • 两种方式实现

 //在 TypeScript 看来,greaterThan2 的类型既可能是数字,也可能是 undefined,所以上面的示例中提示了一个 ts(2322) 错误,此时我们不能把类型 undefined 分配给类型 number
 const arrayNumber: number[] = [1, 2, 3, 4];
 const greaterThan2: number = arrayNumber.find(num => num > 2); // 提示 ts(2322)
 ​
 // 尖括号 语法
 let someValue: any = "this is a string";
 let strLength: number = (<string>someValue).length;
 ​
 // as 语法
 let someValue: any = "this is a string";
 let strLength: number = (someValue as string).length;

非空断言!

  • 排除nullundefined
 function printMessageLength(message?: string) {
   // vue3源码
   console.log(message!.length) //message! 非空
 }
 ​
 printMessageLength("aaaa")
  • 确定赋值断言

    • let x!: number; 确定赋值断言,TypeScript 编译器就会知道该属性会被明确地赋值。
 let x!: number;
 initialize();
 console.log(2 * x); // Ok
 ​
 function initialize() {
   x = 10;
 }

双重断言

  • 如果在使用类型断言时,原类型与断言类型之间差异过大,也就是指鹿为马太过离谱,离谱到了指鹿为霸王龙的程度,TypeScript 会给你一个类型报错:
 const str: string = "Kylin";
 ​
 // 从 X 类型 到 Y 类型的断言可能是错误的,blabla
 (str as { handler: () => {} }).handler()
 ​
 // 它会提醒你先断言到 unknown 类型,再断言到预期类型,就像这样:
 const str: string = "Kylin";
 ​
 (str as unknown as { handler: () => {} }).handler();
 ​
 // 使用尖括号断言
 (<{ handler: () => {} }>(<unknown>str)).handler();
 ​

类型层级

  • 最顶级的类型,anyunknown
  • 特殊的 Object ,它也包含了所有的类型,但和 Top Type 比还是差了一层
  • StringBooleanNumber 这些装箱类型
  • 原始类型与对象类型
  • 字面量类型,即更精确的原始类型与对象类型嘛,需要注意的是 nullundefined 并不是字面量类型的子类型
  • 最底层的 never

Day4

类型别名

  • type用于定义类型别名
 // 联合类型
 type IDType = string | number | boolean
 // 对象类型
 type PointType = {
   x: number
   y: number
   z?: number
 }
 // 函数类型
 type Handler = (e: Event) => void;
 ​
 const clickHandler: Handler = (e) => { };
 const moveHandler: Handler = (e) => { };
 const dragHandler: Handler = (e) => { };
 ​
 function printId(id: IDType) {}
 function printPoint(point: PointType) {}
  • 工具类型

    • 类型别名可以这么声明自己能够接受泛型(我称之为泛型坑位)
    • 泛型参数的名称(上面的 T )也不是固定的。通常我们使用大写的 T / K / U / V / M / O ...这种形式。
    • 对于工具类型来说,它的主要意义是基于传入的泛型进行各种类型操作,得到一个新的类型
 type Factory<T> = T | number | string;
 // 一般不会直接使用工具类型来做类型标注,而是再度声明一个新的类型别名:
 type FactoryWithBool = Factory<boolean>;
 const foo: FactoryWithBool = true;

联合类型|与交叉类型&

  • 联合类型只需要符合成员之一即可(||),而交叉类型需要严格符合每一位成员(&&)。
 interface NameStruct {
   name: string;
 }
 interface AgeStruct {
   age: number;
 }
 ​
 type ProfileStruct = NameStruct & AgeStruct;
 const profile: ProfileStruct = {
   name: "linbudu",
   age: 18
 }
 ​
 // 新的类型会同时符合交叉类型的所有成员,不存在既是 string 又是 number 的类型
 // 这也是 never 这一 BottomType 的实际意义之一,描述根本不存在的类型
 type StrAndNum = string & number; // never
  • 对于对象类型的交叉类型,其内部的同名属性类型同样会按照交叉类型进行合并
 type Struct1 = {
   primitiveProp: string;
   objectProp: {
     name: string;
   }
 }
 ​
 type Struct2 = {
   primitiveProp: number;
   objectProp: {
     age: number;
   }
 }
 ​
 type Composed = Struct1 & Struct2;
 ​
 type PrimitivePropType = Composed['primitiveProp']; // never
 type ObjectPropType = Composed['objectProp']; // { name: string; age: number; }
 ​

索引类型

  • 包含三个部分:索引签名类型索引类型查询索引类型访问

索引签名类型

  • 如下格式:
 interface A {
     [key: string]: string
 }

索引类型查询

  • keyof操作符,返回索引所有key对应类型字面量的联合类型

    • 这里并不会将数字类型的键名转换为字符串类型字面量,而是仍然保持为数字类型字面量
 interface Foo {
   Kylin: 1,
   599: 2
 }
 ​
 type FooKeys = keyof Foo; // "Kylin" | 599
 // 在 VS Code 中悬浮鼠标只能看到 'keyof Foo'
 // 看不到其中的实际值,你可以这么做:
 type FooKeys = keyof Foo & {}; // "Kylin" | 599
  • 模拟 “从键名到联合类型” 的过程。
 type FooKeys = Object.keys(Foo).join(" | ");
 export const SIZES = ['mini', 'small', 'medium', 'large'] as const;
 export type Size = typeof SIZES[number];
  • 这段代码定义了一个常量数组 SIZES,其中包含了四个字符串元素:minismallmediumlarge。同时,使用 as const 关键字将数组中的元素类型设为不可变常量类型。
  • 接下来,定义了一个类型别名 Size,它的类型是 typeof SIZES[number]。这个类型别名的意思是,Size 的类型是 SIZES 数组中所有元素的联合类型。[number] 表示数组的索引类型,typeof 表示获取 SIZES 的类型,因此 typeof SIZES[number] 表示获取 SIZES 数组中所有元素的类型,并将它们组成一个联合类型

img

img

映射类型

  • 映射类型只能使用类型别名实现
  • in是遍历的意思

image-20231221142535166

type Stringify<T> = {
  [K in keyof T]: string;
};
  • [K in keyof T]: string是一个映射类型(Mapped Type)的语法。这个特定的映射类型 Stringify<T> 会将一个类型 T 的所有属性的类型转换成 string 类型
interface Foo {
  prop1: string;
  prop2: number;
  prop3: boolean;
  prop4: () => void;
}

type StringifiedFoo = Stringify<Foo>;

// 等价于
interface StringifiedFoo {
  prop1: string;
  prop2: string;
  prop3: string;
  prop4: string;
}

Day5

类型查询操作符

  • typeof:用于基本类型

类型守卫

is

  • is 关键字 + 预期类型。如果这个函数成功返回为 true,那么 is 关键字前这个入参的类型,就会被这个类型守卫调用,方便后续的类型控制流分析
 function isString(input: unknown): input is string {
   return typeof input === "string";
 }
 ​
 function foo(input: string | number) {
   if (isString(input)) {
     // 正确了
     (input).replace("linbudu", "linbudu599")
   }
   if (typeof input === 'number') { }
   // ...
 }

in

  • 通过 key in object 的方式来判断 key 是否存在于 object 或其原型链上(返回 true 说明存在)。
 interface Foo {
   foo: string;
   fooOnly: boolean;
   shared: number;
 }
 ​
 interface Bar {
   bar: string;
   barOnly: boolean;
   shared: number;
 }
 ​
 function handle(input: Foo | Bar) {
   if ('foo' in input) {
     input.fooOnly;
   } else {
     input.barOnly;
   }
 }

instanceof

  • 类似于 typeof 与 in 的操作符,用于引用类型,包括判断A是否是B的实例
 class FooBase {}
 ​
 class BarBase {}
 ​
 class Foo extends FooBase {
   fooOnly() {}
 }
 class Bar extends BarBase {
   barOnly() {}
 }
 ​
 function handle(input: Foo | Bar) {
   if (input instanceof FooBase) {
     input.fooOnly();
   } else {
     input.barOnly();
   }
 }

类型断言守卫asserts

  • 断言守卫和类型守卫最大的不同点在于,在判断条件不通过时,断言守卫需要抛出一个错误,类型守卫只需要剔除掉预期的类型。
 function assert(condition: any, msg?: string): asserts condition {
   if (!condition) {
     throw new Error(msg);
   }
 }

接口合并

  • 继承extends
  • 接口合并,这些同名属性的类型仍然需要兼容
 interface Struct1 {
   primitiveProp: string;
   objectProp: {
     name: string;
   };
   unionProp: string | number;
 }
 ​
 // 接口“Struct2”错误扩展接口“Struct1”。
 interface Struct2 extends Struct1 {
   // “primitiveProp”的类型不兼容。不能将类型“number”分配给类型“string”。
   primitiveProp: number;
   // 属性“objectProp”的类型不兼容。
   objectProp: {
     age: number;
   };
   // 属性“unionProp”的类型不兼容。
   // 不能将类型“boolean”分配给类型“string | number”。
   unionProp: boolean;
 }