TS 学习日记

179 阅读19分钟

TypeScript 的重要性以及难点

重要性

js 要想开发大型项目最起码符合开发大型项目的基本条件,基本条件我认为有两条:

  • 模块化
  • 类型检查

模块化可以降低开发的心智负担,不用总担心会有命名冲突,以前自己写点小代码时,总喜欢使用什么 name,top 作为变量名称,总会遇到莫名其妙的bug,后面才发现这两个都是 window 的属性,而且这个 top 还没没法修改,那时我写代码时总担心命名冲突,后面学到了模块化,不涉及页面的时候尽量使用 node,要使用页面我也会开启模块化,这种开发体验就舒适很多了。我写点小代码没有模块化都会出些 bug,更何况是线上的项目呢,因此模块化是必需品。

然后就是类型检查

说实话,我其实更希望 js 本身能出一套强类型标准,我一直觉得写 ts 好别扭,就是感觉有种不适配的那种感觉,没有 java 的那种强类型自然。但是可能性不大,要出早出了,我觉得不出是考虑到以前的老项目吧。但是类型检查确实很有必要,据统计 js 代码报错最多的提示就是 不能在 undefined 读什么什么值,这也和我开发体验一致。类型检查的话可以在项目开发时就帮我们规避掉一些问题,而且也会给到更好的提示。关键是后期的维护与扩展也会更加容易。

其实与其说 ts 重要,不如说类型检查重要,也就是说 ts 就是 js 为了实现类型检查的工具罢了,所以可以放松心态,学习 ts 就当是学习 js 的类型检查,js 内容那么多都学过来了,就一个类型检查有什么搞不定的。

难点

其实我一直认为 ts 本身并不难,它无非就是给变量做类型标注,难的加入类型标注之后带来的一些变化。

第一就是开发思维。

js 虽然说啥一门面向对象的语言,但是在实际开发过程中却基本上都是面向过程开发。在增加了类型检查之后,其实就是在增强 js 的面向对象的能力,ts 提供了像抽象类、字段修饰符这些东西,完善了 js 的面向对象的能力,所以说开发思维的转换会比较有难度。

第二就是类型演算

就是类型与类型之间的关系,如何在保证类型安全的前提下,优雅的表示类型与类型之间的关系,确实挺有难度的。

基本类型

基本语法

let str: string = '1';
let num: number = 1;
let bol: boolean = false;
let obj: object = {};
let cat: { name: string, age: number } = {
    name: "cat",
    age: 3
}
let fn: (x: number, y: number) => number = function (a: number, b: number) {
    return a + b;
}

就是在变量后面打个冒号写一下类型

js 基本数据类型

首先的话就是 js 的基本数据类型 null 、undefined、string、number、boolean、symbol、bigint、object。

这块的话没什么好说,该是什么就是什么。

联合类型

//语法
let a: string | number; 
//也可以联合更多类型类型与类型之间用 | 隔开。
//语法
a = "1";
a = 1;
//联合类型中的任意一个类型都可以

交叉类型

交叉类型与联合类型相反,他说需要满足所有的这个类型,一般说用于对象,当然也可以用于基本数据类型,就把基本数据类型看成包装类。

let qq: { name: string } & { age: number } = {
    name: "1",
    age: 2
}
//从这里看好像不如直接写到一个类型中,可是在实际开发中吗,
//通常是联合已经写好了的对象,所以还是挺有用的

type

//定义对象
type Point = {
    x: number;
    y: number;
    sayHello(): void
};
//定义方法
type A = {
    (a: number, b: number): void;
    age: number;
    name: string
}
//定义方法
type B = (a: number, b: number) => void;
//定义联合类型
type C = "Red" | "Black";

直接写在变量后面都类型标注,都可以通过 type 来接收类型

interface

interface 是定义对象的另一种方式,是的 type 可以定义的类型范围更广。

//定义对象
interface Point  {
    x: number;
    y: number;
    sayHello(): void
};
//定义方法
interface A  {
    (a: number, b: number): void;
    age: number;
    name: string
}

枚举 enum

enum A { 'red', 'black', 'white' }

//编译成 js 后
"use strict";
var A;
(function (A) {
    A[A["red"] = 0] = "red";
    A[A["black"] = 1] = "black";
    A[A["white"] = 2] = "white";
})(A || (A = {}));
//使用时可以通过下标去访问
enum A { 'red' = 'red', 'black' = 'black', 'white' = 'white' }

//编译后
"use strict";
var A;
(function (A) {
    A["red"] = "red";
    A["black"] = "black";
    A["white"] = "white";
})(A || (A = {}));
//直接通过键名访问

元组

元组中位置的类型都是确定的

type StringNumberPair = [string, number];

function doSomething(pair: [string, number]) {
  const a = pair[0];
       
// const a: string
  const b = pair[1];
       
// const b: number
  // ...
}
 
doSomething(["hello", 42]);

也可以定义不定量的元组

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

类型断言

interface Pet {
    name: string
}
class MyPet implements Pet {
    name: string = "pet"

}
function isPet(test: any): boolean {
    return test instanceof MyPet;
}
function a(): any {
    return Math.random() > 0.5 ? new MyPet() : 1
}
let str = a();
if (isPet(str)) {
    str;
}

转存失败,建议直接上传图片文件

上面这段代码尽管我们已经知道了 str 的类型,但是 ts 却不能给出类型提示,这个时候可以进行类型断言

interface Pet {
    name: string
}
class MyPet implements Pet {
    name: string = "pet"

}
function isPet(test: any): boolean {
    return test instanceof MyPet;
}
function a(): any {
    return Math.random() > 0.5 ? new MyPet() : 1
}
let str = a();
if (isPet(str)) {

//语法就是使用 as 来修饰
    (str as Pet).name;
}

转存失败,建议直接上传图片文件

更多内容: www.typescriptlang.org/docs/handbo…

类型保护

typeof

使用 typeof 进行类型判断后,在这个分支,会缩小类型的范围,给出更好的类型提示。

function printAll(strs: string | string[] | null) {
  if (typeof strs === "object") {
    for (const s of strs) {
// 'strs' is possibly 'null'.
      console.log(s);Ï
    }
  } else if (typeof strs === "string") {
    console.log(strs);
  } else {
    // do nothing
  }
}

equal

通过相等判断也能够缩小类型的范围

function example(x: string | number, y: string | boolean) {
  if (x === y) {
    // We can now call any 'string' method on 'x' or 'y'.
    x.toUpperCase();
          
// (method) String.toUpperCase(): string
    y.toLowerCase();
          
// (method) String.toLowerCase(): string
  } else {
    console.log(x);
               
// (parameter) x: string | number
    console.log(y);
               
// (parameter) y: string | boolean
  }
}

in与instanceof

这两个都是同样的道理

type Fish = { swim: () => void };
type Bird = { fly: () => void };
 
function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    return animal.swim();
  }
 
  return animal.fly();
}

is

is 是一个类型谓语,可以帮助 ts 更好的进行类型判断,他类似于 boolean 但是和 boolean 相比,可以帮助ts 缩小类型范围,如代码所示。

function isString(test: any): boolean {
    return test instanceof String;
}

function a(): any {
    return Math.random() > 0.5 ? 1 : "1";
}
let str =a();
if(isString(str)){
    str
}

转存失败,建议直接上传图片文件

使用 boolean 来判断时,判断即使判断通过,ts 也不认为它是 string 类型,即使我们已经确认它是一个 string 这个时候我们就可以使用 is 来进行类型保护

function isString(test: any): test is string {
    return typeof test === "string";
}

function a(): any {
    return Math.random() > 0.5 ? 1 : "1";
}
let str =a();
if(isString(str)){
    str
}

转存失败,建议直接上传图片文件

通过这种方式 ts 就能够知道代码块中的变量是一个 string。总的来说就是 is 返回值会是一个 boolean,但是他可以更好的进行类型保护,当然,我们也可以通过类型断言进行一个类型判断,不过使用得多的话,is 这种方式会更加简洁,因为只需要写一次。

更多内容: www.typescriptlang.org/docs/handbo…

函数

函数定义

  1. 函数表达式
function greeter(fn: (a: string) => void) {
  fn("Hello, World");
}
 
function printToConsole(s: string) {
  console.log(s);
}
 
greeter(printToConsole);

2. type 定义

type GreetFunction = (a: string) => void;
function greeter(fn: GreetFunction) {
  // ...
}

3. interface 定义

interface GreetFunction {
    (a: string): void;
}
function greeter(fn: GreetFunction) {
    // ...
}

4. 定义构造函数

type SomeConstructor = {
    new(s: string): SomeObject;
};
function fn(ctor: SomeConstructor) {
    return new ctor("hello");
}

函数重载

在调用的时候可以获得更好的类型检查,实现的时候需要自己判断

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

函数感觉也没啥好看的,和 js 的差不了多少,有啥问题直接查文档吧。

www.typescriptlang.org/docs/handbo…

对象以及类型操作符

基本语法

和函数一样对象的表示也可以使用 type 和 interface 两种形式

//使用 interface
interface Person {
  name: string;
  age: number;
}
//使用 type
type Person = {
    name: string;
    age: number
}

有一点要注意就是定义对象的方法和直接定义方法语言有点相似容易搞混。

//定义方法
interface Fn {
   (x: number, y: number):number
}
//定义对象
interface Obj {
    add: (x: number, y: number) => number
}

咋一看确实有一点像,可以仔细看确实能看出来区别。

属性修饰符

readonly,就和它的语义一样,只能读不能修改。

interface SomeType {
  readonly prop: string;
}
 
function doSomething(obj: SomeType) {
  // We can read from 'obj.prop'.
  console.log(`prop has the value '${obj.prop}'.`);
 
  // But we can't re-assign it.
  obj.prop = "hello";
// Cannot assign to 'prop' because it is a read-only property.
}

?修饰符,js 的?差不多,意思是可选属性,赋值的时候熟悉可有可无不会引起类型报错

interface PaintOptions {
  shape: Shape;
  xPos?: number;
  yPos?: number;
}
 function paintShape(opts: PaintOptions) {
  let xPos = opts.xPos;
// (property) PaintOptions.xPos?: number | undefined
  let yPos = opts.yPos;
// (property) PaintOptions.yPos?: number | undefined
}

索引签名

有些时候我们不知道属性的键名,但是却了解值的类型,这个时候就可以使用索引签名。

interface StringArray {
  //这个 index 是个变量,符合变量命名规范,叫什么都行,ts 中各种定义的语法还是有点容易搞混。
  [index: number]: string;
}

const myArray: StringArray = getStringArray();
const secondItem = myArray[1];

泛型

ts 为什么需要泛型呢,因为在强类型语言,有些时候类型在定义是不确定的,在调用的时候才能确定,但是类型和类型之间却是存在一定的逻辑关系,确定了一个类型,则可以推断出一些其它的类型,这个时候我们使用泛型来表示类型之间的关系。

//我是一个负责日志打印的中间件,给我什么类型我就返回什么类型。
function LoggerString(target: string): string {
    console.log(target)
    return target
}
function LoggerBoolean(target: boolean): boolean {
    console.log(target)
    return target
}
function LoggeNumberr(target: number): number {
    console.log(target)
    return target
}

从上图看参数的类型和返回值的类型是一样的,但是却不得不写多个函数来进行类型的判定,这是非常不方便的,在这种条件下使用泛型是非常合适的

function Logger<T>(target: T): T {
    console.log(target)
    return target
}
Logger(1);
Logger("!");
Logger(false);

这里说的比较简单,建议是直接看官网学习。

更多信息 www.typescriptlang.org/docs/handbo…

Keyof

Keyof 还是挺有趣的,他会将对象所有的键组成的字符串或数字的文字联合作为其类型

type Point = { x: number; y: number };
type P = keyof Point;
//此时 P 类型其实就是字符串 x 和 y 的联合类型,和如下代码一样;
// type P = "x" | "y";

不过对于索引签名则会直接返回其索引签名的类型

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
//M的类型之所以可以是 number 是因为自动类型转换 obj[0] === obj['0']

上面的例子都是官网的例子,自主学习,强烈建议阅读官网

www.typescriptlang.org/docs/handbo…

Typeof

Typeof 其实没啥好说的,和 js 一样它会返回一个值的类型。

let s = "hello";
let n: typeof s;
   
let n: string

Index Acess Type

也是比较易懂的知识点

type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"];
     
// type Age = number

详细请看 www.typescriptlang.org/docs/handbo…

条件类型

直接看官网的两个例子吧

interface Animal {
  live(): void;
}
interface Dog extends Animal {
  woof(): void;
}
 
type Example1 = Dog extends Animal ? number : string;
        
// type Example1 = number
 
type Example2 = RegExp extends Animal ? number : string;
        
// type Example2 = string

还可以和泛型配合使用

interface IdLabel {
  id: number /* some fields */;
}
interface NameLabel {
  name: string /* other fields */;
}
 
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 number | string> = T extends number
  ? IdLabel
  : NameLabel;

条件类型这一块还有一个小的知识点就是这个 infer

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
//infer 在这一块就是推断你有返回值,有返回值直接返回返回值的类型,否则返回 any 类型
type fn = () => number
type fnReturnType = ReturnType<fn> // number
//意思就是函数返回值是什么这个 ReturnType 就返回什么。

加个例子加深一下印象

// infer 在推断 promise 的泛型类型,直接返回 promise 的泛型类型
type PromiseResType<T> = T extends Promise<infer R> ? R : T

// 验证
async function strPromise() {
  return 'string promise'
}

interface Person {
  name: string;
  age: number;
}
async function personPromise() {
  return {
    name: 'p',
    age: 12
  } as Person
}

type StrPromise = ReturnType<typeof strPromise> // Promise<string>
// 反解
type StrPromiseRes = PromiseResType<StrPromise> // str

type PersonPromise = ReturnType<typeof personPromise> // Promise<Person>
// 反解
type PersonPromiseRes = PromiseResType<PersonPromise> // Person

也可以去推断数组

type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
//意思其实就是判断一下这个泛型是不是一个数组是数组的话,
//返回数组子项的类型,否则直接返回泛型的类型

转存失败,建议直接上传图片文件

转存失败,建议直接上传图片文件

官网还有更多例子 www.typescriptlang.org/docs/handbo…

Mapped Types

对这个映射类型,我的理解就是做高级类型的,就是在现有类型的基础之上,增加功能点

type OptionsFlags<Type> = {
    [Property in keyof Type]?: Type[Property];
};
//比如这个类型可以将泛型中的所有属性全部变成可选属性
interface AAA {
    name: string;
    age: number;
}

type BBB = OptionsFlags<AAA>

转存失败,建议直接上传图片文件

在映射期间可以应用两个附加修饰符:readonly和?分别影响可变性和可选性。可以通过添加-或前缀来删除或添加这些修饰符+。如果不添加前缀,则+假定为。

// Removes 'readonly' attributes from a type's properties
type CreateMutable<Type> = {
  -readonly [Property in keyof Type]: Type[Property];
};
 
type LockedAccount = {
  readonly id: string;
  readonly name: string;
};
 
type UnlockedAccount = CreateMutable<LockedAccount>;
           
// type UnlockedAccount = {
//     id: string;
//     name: string;
// }

模板类型

模板类型建立在字符串类型之上,并且能够通过联合扩展到许多字符串。

 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"

基于基本用法有一些可以配合Mapped types进行高阶用法参考 www.typescriptlang.org/docs/handbo…

基本语法

class Point {
  x: number;
  y: number;
}
//基本语法和 es6 无差别,毕竟 ts 是 js 的超集

修饰符

readonly 和对象中的用法一致

public 使用 public 声明的变量和方法到处都可以使用,不显示的修饰符,默认都是都是 public 不过权限修饰符和 readonly 并不冲突。

class Greeter {
  public greet() {
    console.log("hi!");
  }
}
const g = new Greeter();
g.greet();

protetced 仅自身和子类可以使用。

class Greeter {
  public greet() {
    console.log("Hello, " + this.getName());
  }
  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.greet(); // OK
g.getName();
// Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.

private 只允许自身使用

class Base {
  private x = 0;
}
const b = new Base();
// Can't access from outside the class
console.log(b.x);
// Property 'x' is private and only accessible within class 'Base'.

class Base {
  private x = 0;
}
class Derived extends Base {
Class 'Derived' incorrectly extends base class 'Base'.
  // Property 'x' is private in type 'Base' but not in type 'Derived'.
  x = 1;
}

通用类

类与接口非常相似,可以是通用的。当使用 实例化泛型类时new,其类型参数的推断方式与函数调用中的方式相同。

class Box<Type> {
  contents: Type;
  constructor(value: Type) {
    this.contents = value;
  }
}
 
const b = new Box("hello!");

类这一块,梳理得比较简单,因为实话实说,我感觉 ts 定义类型大部分时候使用的都是 type 与 interface,不过文档中类的内容还是挺多的,而且 js 支持的 ts 肯定支持 包括属性描述符之类的。

详细请看 www.typescriptlang.org/docs/handbo…

TypeSCript 内置高级类型

官方资料 www.typescriptlang.org/docs/handbo…

我觉得内置高级类型这一块,对象那块理解了的话,这个可以直接配合源码看,挑一些出来看看源码;

Awaited

返回 promise 成功后的类型

//使用
type A = Awaited<Promise<string>>;
    
type A = string
 
type B = Awaited<Promise<Promise<number>>>;
    
type B = number
 
type C = Awaited<boolean | Promise<number>>;
    
type C = number | boolean
//源码
type Awaited<T> = T extends null | undefined //判断 T 是不是为 null 或者 undefined
  ? T //是 null 或者 undefined 直接返回
  : T extends object & { then(onfulfilled: infer F, ...args: infer _): any } // 判断 T 是不是一个 Promise
  ?F extends (value: infer V, ...args: infer _) => any //判断一下是否注册了 onfulfilled 方法
    ? Awaited<V> //注册了就递归调用
    : never //没有注册则永远不会返回值,所以为never
  : T; //不是 Promise 则直接返回

Partial

将类型中的所有熟悉设置为可选

//使用
interface Todo {
  title: string;
  description: string;
}
 
function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
  return { ...todo, ...fieldsToUpdate };
}
 
const todo1 = {
  title: "organize desk",
  description: "clear clutter",
};
 
const todo2 = updateTodo(todo1, {
  description: "throw out trash",
});


//源码
type Partial<T> = {
    [P in keyof T]?: T[P];//这个还是比较简单就把所有熟悉的类型都变成了可选类型
};

Required

将所有的可选属性全都去掉

//使用
interface Props {
  a?: number;
  b?: string;
}
 
const obj: Props = { a: 5 };
 
const obj2: Required<Props> = { a: 5 };
// Property 'b' is missing in type '{ a: number; }' but required in type 'Required<Props>'.

//源码
type Required<T> = {
    [P in keyof T]-?: T[P]; //没啥好说的和 Partial 一样
};

Readonly

将所有的属性都设为只读

//使用
interface Todo {
  title: string;
}
 
const todo: Readonly<Todo> = {
  title: "Delete inactive users",
};
 
todo.title = "Hello";
// Cannot assign to 'title' because it is a read-only property.

//源码
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

Record

构造一个新的对象类型,值的类型统一

//使用
interface CatInfo {
  age: number;
  breed: string;
}
 
type CatName = "miffy" | "boris" | "mordred";
 
const cats: Record<CatName, CatInfo> = {
  miffy: { age: 10, breed: "Persian" },
  boris: { age: 5, breed: "Maine Coon" },
  mordred: { age: 16, breed: "British Shorthair" },
};
 
cats.boris;
 
const cats: Record<CatName, CatInfo>
//源码
type Record<K extends keyof any, T> = {
    [P in K]: T;
};

Pick

选择我要的属性

//使用
interface Todo {
  title: string;
  description: string;
  completed: boolean;
}
 
type TodoPreview = Pick<Todo, "title" | "completed">;
 
const todo: TodoPreview = {
  title: "Clean room",
  completed: false,
};
 
todo;
 
const todo: TodoPreview

//源码
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

Omit

筛选掉我不需要的属性

//使用
interface Todo {
  title: string;
  description: string;
  completed: boolean;
  createdAt: number;
}
 
type TodoPreview = Omit<Todo, "description">;
 
const todo: TodoPreview = {
  title: "Clean room",
  completed: false,
  createdAt: 1615544252770,
};
 
todo;
 
const todo: TodoPreview
 
type TodoInfo = Omit<Todo, "completed" | "createdAt">;
 
const todoInfo: TodoInfo = {
  title: "Pick up kids",
  description: "Kindergarten closes at 5pm",
};
 
todoInfo;
   
const todoInfo: TodoInfo
//源码
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
//拿到了 T 有 K 没有的所有键名,利用 Pick 把它拿到,就完成了 Omit
Exclude<keyof T, K>
//顺带把 Exclude 的源码也贴了,看链接
type Exclude<T, U> = T extends U ? never : T;

我开始看见这个 Exclude 的返回结果不是很理解,去找一了一下。

链接 segmentfault.com/q/101000002…

Extract

与 Exclude 相反。

NonNullable

去掉类型中的 null 以及 undefined

//使用
type T0 = NonNullable<string | number | undefined>;
     
type T0 = string | number
type T1 = NonNullable<string[] | null | undefined>;
     
type T1 = string[]

//源码
//这块咋一看不理解,其实把所有的基本类型看成包装类型就能理解了
type NonNullable<T> = T & {};

Parameters

返回函数的参数列表类型的元组

//使用
declare function f1(arg: { a: number; b: string }): void;
 
type T0 = Parameters<() => string>;
     
type T0 = []
type T1 = Parameters<(s: string) => void>;
     
type T1 = [s: string]
type T2 = Parameters<<T>(arg: T) => T>;
     
type T2 = [arg: unknown]
type T3 = Parameters<typeof f1>;
     
type T3 = [arg: {
    a: number;
    b: string;
}]
type T4 = Parameters<any>;
     
type T4 = unknown[]
type T5 = Parameters<never>;
     
type T5 = never
type T6 = Parameters<string>;
Type 'string' does not satisfy the constraint '(...args: any) => any'.
     
type T6 = never
type T7 = Parameters<Function>;
Type 'Function' does not satisfy the constraint '(...args: any) => any'.
  Type 'Function' provides no match for the signature '(...args: any): any'.
     
type T7 = never
//源码,这个主要要理解 infer 这个关键字,在对象的条件类型中提到了
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

ConstructorParameters

返回构造函数的参数列表元组,与 Parameters 类似。

ReturnType

返回函数的返回值类型,远离与上面两个类似

//使用
declare function f1(): { a: number; b: string };
 
type T0 = ReturnType<() => string>;
     
type T0 = string
type T1 = ReturnType<(s: string) => void>;
     
type T1 = void
type T2 = ReturnType<<T>() => T>;
     
type T2 = unknown
type T3 = ReturnType<<T extends U, U extends number[]>() => T>;
     
type T3 = number[]
type T4 = ReturnType<typeof f1>;
     
type T4 = {
    a: number;
    b: string;
}
type T5 = ReturnType<any>;
     
type T5 = any
type T6 = ReturnType<never>;
     
type T6 = never
type T7 = ReturnType<string>;
// Type 'string' does not satisfy the constraint '(...args: any) => any'.
     
type T7 = any
type T8 = ReturnType<Function>;
// Type 'Function' does not satisfy the constraint '(...args: any) => any'.
  // Type 'Function' provides no match for the signature '(...args: any): any'.
     
type T8 = any

//源码
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

InstanceType

返回构造函数的返回值

//使用
class C {
  x = 0;
  y = 0;
}
 
type T0 = InstanceType<typeof C>;
     
type T0 = C
type T1 = InstanceType<any>;
     
type T1 = any
type T2 = InstanceType<never>;
     
type T2 = never
type T3 = InstanceType<string>;
Type 'string' does not satisfy the constraint 'abstract new (...args: any) => any'.
     
type T3 = any
type T4 = InstanceType<Function>;
Type 'Function' does not satisfy the constraint 'abstract new (...args: any) => any'.
  Type 'Function' provides no match for the signature 'new (...args: any): any'.
     
type T4 = any

//源码
type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any;

自定义高级类型

这些高级类型都是看了大佬的内容,做个记录

DeepReadonly

官方提供的 Readonly 是一个浅层次的不可变,有时候想要深层次不可变对象

type DeepReadonly<T extends Record<string | symbol, any>> = {
  readonly [p in keyof T]: DeepReadonly<T[p]>;
};

结果演示

转存失败,建议直接上传图片文件

基于这个思路也可以去做一些类似的类型,如 DeepRequire。

UnionToIntersection

可以将联合类型转换为交叉类型

type UnionToIntersection<T> = (T extends any ? (x: T) => any : never) extends (x: infer R) => any ? R : never;
type Test = { a: string } | { b: string } | { c: string };
type Test2 = UnionToIntersection<Test>;

转存失败,建议直接上传图片文件

Optional

将部分字段变成可选字段

type Optional<T, V extends keyof T> = Omit<T, V> & Partial<Pick<T, V>>;

interface Article {
    title: string;
    content: string;
    author: string;
    date: Date;
    readCount: number;
}
type OptionalArtilce = Optional<Article, "title" | "date">;
function A(article: OptionalArtilce) {
    article 
}

转存失败,建议直接上传图片文件

按照这个思路自己也能开发一下类似于部分只读,部分必选等高级类型

Weaken

开发中有时候需要修改类型中的一个或几个属性的类型,但是修改后会导致前面使用的类型报错,这个时候我们就可以使用 Weaken。

interface Test {
  name: string
  say(word: string): string
}

interface Test2  extends Test{
  name: Test['name'] | number
}
// error: Type 'string | number' is not assignable to type 'string'.
// 原理是,将 类型 T 的所有 K 属性置为 any,
// 然后自定义 K 属性的类型,
// 由于任何类型都可以赋予 any,所以不会产生冲突
type Weaken<T, K extends keyof T> = {
  [P in keyof T]: P extends K ? any : T[P];
};

interface Test2  extends Weaken<Test, 'name'>{
  name: Test['name'] | number
}

但是我发现他这里这么用也有点不好,如下

转存失败,建议直接上传图片文件

假如有更多的属性需要修改的话,他的修改逻辑是一样的就会产生重复代码,因此,我做了一些优化

interface Test {
    name: string;
    age: number;
    say(word: string): string
}

type Weaken<T, K extends Partial<Record<keyof T ,any>>> = {
    [P in keyof T]: P extends keyof K ? K[P] : T[P];
};
type Test2 =   Weaken<Test, {age:string,name:number}>

效果如下

转存失败,建议直接上传图片文件

Unpacked

有时候我们可能想抽离一些比较重要的类型

type Unpacked<T> =
    T extends (infer U)[] ? U :
    T extends (...args: any[]) => infer U ? U :
    T extends Promise<infer U> ? U :
    T;

type T0 = Unpacked<string>;  // string
type T1 = Unpacked<string[]>;  // string
type T2 = Unpacked<() => string>;  // string
type T3 = Unpacked<Promise<string>>;  // string
type T4 = Unpacked<Promise<string>[]>;  // Promise<string>
type T5 = Unpacked<Unpacked<Promise<string>[]>>;  // string

装饰器

装饰器是面向对象的概念(java:注解,C#:特征),Decorator 在 Angular 中大量使用。目前在 es 标准中处于 stage3,可以了解一下各个阶段的标准。tc39.es/process-doc…,等到 stage4 就正式纳入 es标准了。不过感觉目前前端使用的地方越来越多了,主要作用就是分离关注点以及解决部分重复代码,可以提前了解一下其预案。github.com/tc39/propos…。简单来说装饰器就是为某些属性、类、参数、方法提供元数据信息,元数据就是描述数据的数据。

类装饰器

类装饰器的本质是一个函数,该函数接收一个参数,表示类本身

    function test(target: new () => object) {
      console.log(target);
    }
    @test
    class A {}

装饰器函数会在类定义后运行,有兴趣可以看一下编译后的代码

    var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
        var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
        if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
        else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
        return c > 3 && r && Object.defineProperty(target, key, r), r;
    };
    function test(target) {
        console.log(target);
    }
    var A = /** @class */ (function () {
        function A() {
        }
        A = __decorate([
            test
        ], A);
        return A;
    }());

类装饰器函数返回值可以是 void 或者是一个新的类,多个装饰器,允许顺序从下到上运行

成员装饰器

属性 属性装饰器需要两个参数

  1. 如果是静态属性,则为类本身,如果是实例属性则是类的原型
  2. 固定为一个字符串为属性名
    type Constructor = new (...args: any[]) => object;
    function d(target:any,key:string){
      console.log(key,target)
    }
    class A{
      @d
      static title:string;
      @d
      title1:string
    }

方法 方法装饰器有三个参数

  1. 如果是静态方法,则为类本身,如果是实例属性则是类的原型
  2. 固定为一个字符串为方法名称
  3. 属性描述符
<!---->
    type Constructor = new (...args: any[]) => object;
    function d(target: any, key: string) {
      console.log(key, target);
    }
    function test(target: any, propName: string, desc: PropertyDescriptor) {

    }
    class A {
      @d
      static title: string;
      @d
      title1: string;
      @test
      say() {}
    }

通常会通过装饰器工厂去保存一些数据信息,简单理解,就是高阶函数,如下。

    let map: Map<string, any> = new Map();
    function test(str: string) {
      return function (target: any, propName: string) {
        map.set(`${target.constructor.name}-${propName}`, 1);
      };
    }
    class A {
      @test("say")
      say() {}
    }
    console.log(map);

不过通常开发时都会使用三方库来进行保存元数据信息 reflect-metadata,还是挺好用的。 链接 www.npmjs.com/package/ref… 用来保存元数据信息 class-validator:www.npmjs.com/package/cla… 用来做数据验证 class-transformer:www.npmjs.com/package/cla… 可以将平面对象转换成类的对象

参数装饰器

基本上不怎么用吧,主要是做依赖注入(AOP)、依赖倒置 参数列表如下

  1. 如果是静态方法,则为类本身,如果是实例属性则是类的原型
  2. 固定为一个字符串为方法名称
  3. 参数列表中的索引
<!---->
    let map: Map<string, any> = new Map();
    function test(str: string) {
      return function (target: any, propName: string, index: number) {
        map.set(`${target.constructor.name}-${propName}`, 1);
      };
    }
    class A {
      say(@test("1") a: string) {}
    }

## 开发中小 Tips
### 妙用 never
never 是一个永远都不会到达的类型,如下代码在 default 分支中 method 就是一个 never 类型。在 default 分支中给一个类型为 n 的 never 类型不会报错。
```ts
    type Method = "GET" | "POST";
    function request(url: string, method: Method):any {
        switch (method) {
            case "GET":
                return 1;
            case "POST":
                return 2;
            default:Ï
                const n: never = method;,
        }
    }

image.png 但是有时候由于项目初期项类型计的不合理,在项目进行时可能会去改一些类型,如下

type Method = "GET" | "POST" | "PUt";//method 增加一个联合类型会报错
function request(url: string, method: Method): any {
    switch (method) {
        case "GET":
            return 1;
        case "POST":
            return 2;
        default:
            const n: never = method;
    }
}

image.png 这种时候如果我们不去增加 PUT 的分支在程序运行时很容易造成 bug,有了这种类型提示可以帮助我们在修改类型的时候就去解决这个问题。

拓展全局变量

有时候我们想扩展全局变量 image.png 问题如上,直接在原型上扩展会报错,因为 String 的原型上面没有这个属性,使用 Object.defineProperty 则是没有类型提示,失去了使用 ts 的初衷,而且调用的时候一样会报错。

String.prototype.sayHello = function () {
}
interface String {
//可以使用 interface 去扩展 String 并不会覆盖。
    sayHello: (...args: any[]) => any
}
let str: String = new String('123');
str.sayHello
//结果如下

image.png

使用三方库想要 TS 的类型检查

其实方式也挺简单的,就在工程中下载一下类型库就好了,如下:

// npm i @types/ndoe  下载 node 的类型包

想要下载哪个类型库,就把 node 替换成哪个库名就好了。有兴趣的话可以去看看哪个知名的库还没有类型库,帮它写类型库,就成为开源贡献者了,不过估计不太可能,哈哈哈哈。

参数结构时给参数类型以及默认值

实话实说参数结构还是挺好用的,不过结构丢失类型还是挺难受的,结构后还想给类型和默认值的话,用下面这种做法

// error
function f({ x: number }) {
    console.log(x);
}

// ok
function f({x}: { x: number } = {x: 0}) {
    console.log(x);
}