《typescript》学习总结笔记

121 阅读28分钟

导读

本文为笔者ts的学习总结笔记,对ts中各种概念进行了提炼,以及配合了一些代码demo

大家可以通过此网站来练习ts写法 www.typescriptlang.org/play

本文将按下面概念来分别介绍ts的用法

  • 一.类型声明
    • 1.type
    • 2.interface
  • 二.类型推断
  • 三.类型分类
    • 1.基本类型
    • 2.值类型
    • 3.any类型
    • 4.unknown类型
    • 5.never类型
    • 6.数组类型
    • 7.元祖类型
    • 8.枚举类型
    • 9.函数类型
    • 10.对象类型
    • 11.class类
  • 四.泛型
  • 五.类型运算
    • 1.交叉
    • 2.联合
    • 3.类型缩小
    • 4.类型兼容
  • 六.类型运算符
    • 1.typeof运算符
    • 2.keyof运算符
    • 3.readonly
    • 4.?
    • 5.in
    • 6.方括号运算符
    • 7.extends
    • 8.infer
    • 9.is
    • 10.模板字符串
  • 七.断言
  • 八.类型工具
    • 1.Awaited
    • 2.ReturnType
    • 3.Parameters
    • 4.ThisParameterType
    • 5.InstanceType
    • 6.ConstructorParameters
    • 7.ThisType
    • 8.OmitThisParameter
    • 9.Exclude<UnionType, ExcludedMembers>
    • 10.Extract<Type, Union>
    • 11.Pick<Type, Keys>
    • 12.Omit<Type, Keys>
    • 13.NonNullable
    • 14.Readonly
    • 15.ReadonlyArray
    • 16.Partial
    • 17.Required
    • 18.Record<Keys, Type>
    • 19.Uncapitalize
    • 20.Capitalize
    • 21.Uppercase
    • 22.Lowercase
  • 类型映射
  • 装饰器
  • 模块
  • namespace
  • declare
  • .d.ts
  • tsconfig

一.类型声明

介绍:用来标注js变量的类型,变量的值应该与声明的类型一致,如果不一致,TypeScript 就会报错

基本使用:冒号+类型

let foo:string;
function toString(num: number): string {
  return String(num)
};
let obj: {
    a: string;
    b: number;
    c: boolean[]
};

复杂类型的声明

有的时候,类型比较复杂,这时候我们可以用type或者interface将类型提炼出来方便复用

1.type

定义一个类型的别名。

  • type可以声明所有类型

    type FnType = (x: string) => void; // 声明函数类型
    const fn: FnType = function(x) {
      console.log(x)
    };
    
    type MyObjType = { // 声明对象类型
        a: string;
        b: number;
        c: any;
        addB(x: number): number;
    }
    const obj: MyObjType = {
        a: 'hello',
        b: 1,
        c: [1, 2, 3],
        addB(x) {
            this.b += x;
            return this.b;
        }
    }
    
    type MyString = 'hello word'; // 声明字面量类型
    const a: MyString = 'hello word';
    
    // ...略
    
    
  • type不允许重名。

      type Color = 'red';
      type Color = 'blue'; // 报错
    
  • type的作用域是块级作用域。这意味着,代码块内部定义的别名,影响不到外部。

      type Color = 'red';
      if (Math.random() < 0.5) {
          type Color = 'blue';
      }                         
    
  • type支持使用表达式

    type Greeting = 'hello';
    type Greeting2 = `${Greeting} word`; // 'hello word'
    

2.interface

用来声明对象模板,它的成员有五种形式

  • 对象属性
  • 属性索引
  • 对象方法
  • 函数
  • 构造函数
interface Person {
    age: number;
    getAge(): number;
    [Prop: String]: any;
}
const xiaoming: Person = {
    age: 18;
    getAge() {
        return this.age;
    };
    name: '小明';
}

interface Fn {
    (x: string): void;
}

const log: Fn = (x) => {
    console.log(x)
}

继承

  • 通过extends关键字实现
  • 如果存在同名属性,则同名属性类型必须相同
  • 也可以继承type, class类型
interface Style {
  color: string;
}
interface Shape {
  name: string;
}

interface Circle extends Style, Shape {
  radius: number;
}

多个同名接口会合并

  • 同名接口合并时,不能出现属性类型冲突
  • 同名接口的方法冲突时会发生方法重载
      interface Document {
        createElement(tagName: any): Element;
      }
      interface Document {
        createElement(tagName: "div"): HTMLDivElement;
        createElement(tagName: "span"): HTMLSpanElement;
      }
      interface Document {
        createElement(tagName: string): HTMLElement;
        createElement(tagName: "canvas"): HTMLCanvasElement;
      }
      // 等同于
      interface Document {
        createElement(tagName: "canvas"): HTMLCanvasElement;
        createElement(tagName: "div"): HTMLDivElement;
        createElement(tagName: "span"): HTMLSpanElement;
        createElement(tagName: string): HTMLElement;
        createElement(tagName: any): Element;
      }
    

interface作为联合类型时,同名属性也是联合类型

    interface Circle {
      area: bigint;
    }

    interface Rectangle {
      area: number;
    }

    declare const s: Circle | Rectangle;
    s.area; // bigint | number

与type的区别

  1. type可以定义所有类型interface只能定义对象类型(对象函数数组
  2. interface可以继承extends,type要想继承往往采用 & (类型交叉)方式
  3. 同名interface会合并,type会报错
  4. interface不包含属性映射,type可以
  5. this只能用于interface
  6. interface无法表达交叉或者联合类型
interface Point {
  x: number;
  y: number;
}

// 正确
type PointCopy1 = {
  [Key in keyof Point]: Point[Key];
};

// 报错,属性映射
interface PointCopy2 {
  [Key in keyof Point]: Point[Key];
};

二.类型推断

  • 类型声明不是必须的,如果没有声明,ts会自动推断类型
    let foo = 123; // 推断foo为number类型
    foo = 'hello'; // 赋值为其他类型就会报错
    
  • 如果无法推断,则认为该变量是any类型
    let a = undefined; // any 
    const b = undefined; // any 
    let c = null; // any 
    const d = null; // any
    

三.类型分类

ts类型可以分为基本类型值类型any类型unknown类型never类型class类型枚举元祖数组函数等,下面将会分别介绍

1.基本类型

  • boolean
  • string
  • number
  • symbol
  • bigint
  • object
  • null
  • undefined

1.boolean只有true和false两种值

const x: boolean = true; 
const y: boolean = false;

2.string类型包含所有字符串

const x: string = "hello";
const y: string = `${x} world`;

3.number包含所有整数和浮点数

const num1: number = 123;
const num2: number = 0.1;
const num3: number = 0xffff;

4.bigint包含所有大整数,且与number不兼容

let x: bigint = 123n
let y: bigint = oxffffn;

x = 123// 报错
y = 204n; // 正确

5.symbol包含所有的Symbol值

const x: symbol = Symbol();

6.object包含了所有的对象函数数组

const x: object = { foo: 123 }; 
const y: object = [1, 2, 3]; 
const z: object = (n: number) => n + 1;

7.undefined,null

undefinednull是两个特殊的类型,他们既是ts里面的类型,也是js的值

const x: undefined = undefined;
const y: null = null;

任何其他类型的变量都可以赋值为undefinednull

let age: number = 24; 
age = null; // 正确 
age = undefined; // 正确
const obj: object = undefined; 
obj.toString(); // 编译不报错,运行就报错

一个没有被声明类型的变量被赋予undefined和null,则他们会被认为any类型

let a = undefined; // any 
const b = undefined; // any 
let c = null; // any 
const d = null; // any

如果希望避免上面这两种情况,可以打开编译选项strictNullChecks

其他类型

2.值类型

ts规定,单个值也是一种类型,称为值类型

let x: "hello"; 
x = "hello"; // 正确 
x = "world"; // 报错

遇到const 声明的变量,如果没有类型声明,则会推断为值类型

// x 的类型是 "https" 
const x = "https"; 
// z 的类型是 { foo: number } 
const z = { foo: 1 };

3.any类型

any称为所有类型的全集,称为顶层类型(top type)

  • any类型表示没有任何限制
    • any类型的变量可以被赋予任意值
    • any类型的变量也可以赋给其它类型
    let x: any;
    x = 'hello'; // 正确
    
    let y: number = 123
    y = x; // 正确
    
  • any会关闭类型检查
    let x: any;
    x = undefined;
    x.toString(); // 正确
    
  • any除了关闭类型检查,还会污染其它变量的类型检查
    let x:any = 'hello';
    let y:number;
    y = x; // 不报错
    y * 123 // 不报错
    y.toFixed() // 不报错
    

4.unknown类型

为了解决any类型污染其它变量的问题,ts3引入了unknown类型; 它被视为除了any的全集,也属于顶层类型

  • 与any的相似
    • 该类型的变量可以被赋予任意类型值
    let x: unknown;
    x = 123;
    x = 'hello'
    
  • 与any的不同
    • 不能直接调用变量的属性和方法
    let v1:unknown = { foo: 123 };
    v1.foo // 报错
    let v2:unknown = 'hello';
    v2.trim() // 报错
    let v3:unknown = (n = 0) => n + 1;
    v3() // 报错
    let x:unknown;
    x = 'hello';
    x.trim() // 报错
    
    • 该类型的变量不能直接赋予其它变量(除了any和unknown)
    let v:unknown = 123;
    let v1:boolean = v; // 报错
    let v2:number = v; // 报错
    
    • 只能进行有限的运算操作 unknown能够进行的运算有限,只能进行比较运算(运算符==、===、!=、!==、||、&&、?)、取反运算(运算符!)、typeof运算符和instanceof运算符

Q: 那么如何使用unknown类型的变量了?

A: 进行类型缩小

let a:unknown = 1;
if (typeof a === 'number') {
    let r = a + 10; // 正确
}

5.never类型

  • 唯一一个底层类型
  • never类型的变量可以赋值给其它类型
function f():never {
    throw new Error('Error');
}
let v1:number = f(); // 不报错
let v2:string = f(); // 不报错
let v3:boolean = f(); // 不报错
  • 不能never类型变量赋予任何值

6.数组类型

数组有一个根本特征:所有成员的类型必须相同,但是成员数量是不确定的,可以是无限数量的成员,也可以是零成员。

  • 数组类型的写法

    • 类型:[]
    let arr: string[] = ['h']
    
    • Array接口
    let arr: Array<number> = [1, 2]
    
  • 联合类型写法

let arr: (string|number)[] = [1,2,3]
let arr2: Array<string|number> = [1,2,3]
  • 类型推断

    • 当空数组没有声明任何类型时,为这个数组赋值时,类型会不断进行更新
    const arr = []; 
    arr; // 推断为 any[] 
    arr.push(123); arr; // 推断类型为 number[] 
    arr.push("abc"); arr; // 推断类型为 (string|number)[]
    
    • 非空数组推断会固定类型
    const arr = [1]; // 推断为number[]类型
    arr.push('a'); // 报错
    
  • 只读数组

    • 说明:不允许改变数组成员
    • 用法
      • 加readonly关键字
      • ReadonlyArray
      • const 断言
    // const arr: ReadonlyArray<number> = [1,2,3];
    // const arr: number[] = [1,2,3] as const;
    const arr: readonly number[] = [1,2,3];
    arr.push(4); // 报错
    arr[0] = 4; //报错

7.元祖类型

元组(tuple)是 TypeScript 特有的数据类型,JavaScript 没有单独区分这种类型。它表示成员类型可以自由设置的数组,即数组的各个成员的类型可以不同。

  • 必须声明每个成员类型

    const s: [string, string, boolean] = ["a", "b", true];

  • 元组成员的类型可以添加问号后缀(?),表示该成员是可选的

    let a: [number, number?] = [1];

  • 使用拓展运算符可以表示不限数量的元祖,可以在任意位置使用

    type tuple = [number, ...number[]]

  • 只读元祖

    // 写法一 
    type t = readonly [number, string]; 
    // 写法二 
    type t = Readonly<[number, string]>;
    

    只读元组是元组的父类型。所以,元组可以替代只读元组,而只读元组不能替代元组。

    type t1 = readonly [number, number]; 
    type t2 = [number, number]; 
    let x: t2 = [1, 2]; 
    let y: t1 = x; // 正确 
    x = y; // 报错
    
    • 只元祖造成的问题
    function distanceFromOrigin([x, y]:[number, number]) { // 
      return Math.sqrt(x**2 + y**2);
    }
    let point = [3, 4] as const; // 只读元祖
    distanceFromOrigin(point); // 报错,因为父类型的值不可以赋给子类型变量
    distanceFromOrigin(point as [number, number]) // 正确
    
  • 成员数量的推断

    • 如果没有可选成员和扩展运算符,TypeScript 会推断出元组的成员数量(即元组长度)。
    function f(point: [number, number]) {
      if (point.length === 3) {
        // 报错
        // ...
      }
    }
    

8.枚举类型

  • 基本使用,成员只能为number或者string类型

    enum Color {
      Red,
      Green,
      Blue,
    }
    // 等同于
    enum Color {
      Red = 0,
      Green = 1,
      Blue = 2,
    }
    
    • 默认为number,从0开始递增,也可以是任意数值,成员的值可以相同
    • 同名enum会合并
      • 合并时只允许其中一个首成员省略初始值
      • 不能出现同名成员
      • 只能都为const枚举或者非const枚举
  • 字符串枚举

    • 基本使用
    enum Direction {
      Up = "UP",
      Down = "DOWN",
      Left = "LEFT",
      Right = "RIGHT",
    }
    
    • 字符串枚举的所有成员值,都必须显式设置。如果没有设置,成员值默认为数值,且位置必须在字符串成员之前
    enum Foo {
      A, // 0
      B = "hello",
      C, // 报错
    }
    
    • 变量类型如果是字符串 Enum,就不能再赋值为字符串,这跟数值 Enum 不一样。
    enum MyEnum {
      One = "One",
      Two = "Two",
    }
    let s = MyEnum.One;
    s = "One"; // 报错
    
  • 数值 Enum 存在反向映射,即可以通过成员值获得成员名。

    enum Weekdays { 
        Monday = 1, 
        Tuesday, 
        Wednesday, 
        Thursday, 
        Friday, 
        Saturday, 
        Sunday,
    } 
    console.log(Weekdays[3]); // Wednesday
    
  • 遍历

    • keyof 返回属性名组成的联合类型
    enum MyEnum { A = "a", B = "b", } 
    // 'A'|'B' 
    type Foo = keyof typeof MyEnum;
    
    • in 返回属性值
    enum MyEnum { A = "a", B = "b", } 
    // { a:any, b: any } 
    type Foo = { [key in MyEnum]: any };
    

9.函数类型

  • 函数的类型声明,需要在声明函数时,给出参数的类型和返回值的类型(可不写,ts会推断),它有三种实现方式
    // 1.直接为函数本身标注类型
    function fn(x:string):void {xxx};
    // 2.箭头函数写法
    type Fn = (x: string) => void;
    // 3.对象写法
    type Fn = {
       (x: string): void 
    }
    
  • 参数
    • 可选参数,只能声明在参数尾部 type Fn = (x?: string) => void
    • 参数默认值
      type Fn = (x: string) => void`
      const fn: Fn = (x = 'hello') => {}
      
    • 参数解构
      function sum(
        { a, b, c }: {
           a: number;
           b: number;
           c: number
        }
      ) {
        console.log(a + b + c);
      }
      
    • rest参数
      function multiply(n:number, ...m:number[]) {
        return m.map((x) => n * x);
      }
      
  • 返回值
    • void类型
      • 没有返回
      • 允许undefined
      • 允许null (strictNullChecks 需关闭)
      • 需要特别注意的是,如果变量、对象方法、函数参数的类型是 void 类型的函数,那么并不代表不能赋值为有返回值的函数。恰恰相反,该变量、对象方法和函数参数可以接受返回任意值的函数,这时并不会报错。
      type voidFunc = () => void;
      const f:voidFunc = () => {
        return 123;
      };
      
    • never类型: 表示肯定不会返回值,抛错的函数或者无限执行的函数
  • 函数重载 有些函数可以接受不同类型或不同个数的参数,并且根据参数的不同,会有不同的函数行为。这种根据参数类型不同,执行不同逻辑的行为,称为函数重载(function overload)。
    function reverse(str: string): string; 
    function reverse(arr: any[]): any[]; 
    function reverse(stringOrArray: string | any[]): string | any[] { 
        if (typeof stringOrArray === "string") return stringOrArray.split("").reverse().join(""); 
        else return stringOrArray.slice().reverse(); 
    }
    
    • 函数重载的每个类型声明之间,以及类型声明与函数实现的类型之间,不能有冲突
    • 重载声明的排序很重要,因为 TypeScript 是按照顺序进行检查的,一旦发现符合某个类型声明,就不再往下检查了,所以类型最宽的声明应该放在最后面,防止覆盖其他类型声明。
    • 函数重载也可以用来精确描述函数参数与返回值之间的对应关系
    type CreateElement = {
      (tag:'a'): HTMLAnchorElement;
      (tag:'canvas'): HTMLCanvasElement;
      (tag:'table'): HTMLTableElement;
      (tag:string): HTMLElement;
    }
    
  • 构造函数:构造函数的实现通常用class写法来表示,构造函数的类型可以在函数前面加new关键字并返回一个class实例类型
    class Animal {
      numLegs:number = 4;
    }
    type AnimalConstructor = new () => Animal;
    function create(c:AnimalConstructor):Animal {
      return new c();
    }
    const a = create(Animal);
    

10.对象类型

除了原始类型,对象是 JavaScript 最基本的数据结构。TypeScript 对于对象类型有很多规则。

对象类型的最简单声明方法,就是使用大括号表示对象,在大括号内部声明每个属性和方法的类型。

const obj: { x: number; y: number; } = { x: 1, y: 1 };

对象属性

  • 属性操作
    • 一旦声明了类型,对象赋值时,就不能缺少指定的属性,也不能有多余的属性。
      type MyObj = { x: number; y: number; }; 
      const o1: MyObj = { x: 1 }; // 报错 
      const o2: MyObj = { x: 1, y: 1, z: 1 }; // 报错
      
    • 读写不存在的属性也会报错。
    • 不能删除属性,修改可以
  • 可选属性
    • 如果某个属性是可选的(即可以忽略),需要在属性名后面加一个问号
    • 可选属性等同于允许赋值为undefined,下面两种写法是等效的
    type User = { firstName: string; lastName?: string; }; 
    // 等同于 
    type User = { firstName: string; lastName: string | undefined; };
    
    -- 可选属性需要判断undefined才能使用
    // 写法一 
    let lastName = user.lastName === undefined ? "Bar" : user.lastName; 
    // 写法二 
    let firstName = user.firstName ?? "Foo"; 
    
  • 只读属性
    • 不能修改,通过readonly关键字修饰
    interface MyInterface { readonly prop: number; }
    
  • 属性名的索引类型: 如果对象的属性非常多,一个个声明类型就很麻烦
    type MyObj = { [property: string]: string; }; 
    const obj: MyObj = { foo: "a", bar: "b", baz: "c", };
    
    • 属性名只支持string,number,symbol
    type T1 = { [property: number]: string; }; 
    type T2 = { [property: symbol]: string; };
    
    • 可以同时多种属性索引,但是不能有冲突,number必须服从string索引
    type MyType = { 
        [x: number]: boolean; // 报错 
        [x: string]: string; 
    };
    type MyType2 = { 
        foo: {a:string; b:number}; // 不报错
        [x: string]: {a:string}; 
    };
    
    • 属性名与属性索引也不能冲突
    type MyType = { 
        foo: boolean; // 报错 
        [x: string]: string; 
    };
    

解构赋值写法

let { x: foo, y: bar }: { x: string; y: number } = obj;

结构类型原则

只要对象 B 满足 对象 A 的结构特征,TypeScript 就认为对象 B 兼容对象 A 的类型,这称为“结构类型”原则(structual typing)。

只要可以使用A的地方,都可以使用B

const B = { x: 1, y: 1, }; 
const A: { x: number } = B; // 正确

严格字面量检查

如果对象使用字面量表示,会触发 TypeScript 的严格字面量检查(strict object literal checking)。如果字面量的结构跟类型定义的不一样(比如多出了未定义的属性),就会报错。

const point: { x: number; y: number; } = {
    x: 1, y: 1, z: 1, 
   // 报错 
};

可以通过变量赋值或者类型断言规避字面量检查

type Mytype = { x: number; y: number; }
let point: Mytype = {
    x: 1, y: 1, z: 1, 
} as Mytype;

const myPoint2 = {
    x: 1, y: 1, z: 1, 
}
point = myPoint2;

或者通过suppressExcessPropertyErrors关闭字面量检查

最小属性原则

如果对象的所有属性都是可选的,那么在赋值的时候至少要有一个属性有值

    type Options = { a?: number; b?: number; c?: number; }; 
    const obj: Options = { d: 123, // 报错 };

空对象

{}可以作为值或者类型,但是该类型的对象不能操作除了原型链上的属性

    const obj = {} 
    // 等同于 const obj:{} = {}
    obj.prop = 123; // 报错

11.class类

  • 属性的类型
    • class的属性可以在顶部声明或者在构造函数里面声明
    class Point { 
        x: number; 
        y: number; 
        constructor(x: number, y: number) {
            this.x = x;
            this.y = y;
        }
    }
    
    // 构造函数里面声明时,修饰符不能省略
    class Point { 
        constructor(public x: number, public y: number) {} 
    }
    
    • 属性如果有初始值则会自动推断类型
    • strictPropertyInitialization打开时需要手动设置初始值, 可以通过类型断言来规避报错
    class Point { 
       x = 1; 
       y!: number; 
    }
    
    • 只读属性只能在构造方法里面修改
  • 方法的类型
    • 类的方法就是普通函数,类型声明方式与函数一致。
     class Point { 
        log(x: string): void {
            console.log(string)
        } 
     }
    
    • 构造方法和方法都支持函数重载,只是构造函数不能设置返回类型
    class Point { 
        constructor(x: number, y: string); 
        constructor(s: string); 
        constructor(xs: number | string, y?: string) { // ... } 
    }
    
  • 静态成员
    • 类的内部可以使用static关键字,定义静态成员
    • 静态成员是只能通过类本身使用的成员,不能通过实例对象使用
    class MyClass { 
        static x = 0; 
        static printX() { console.log(MyClass.x); } 
    } 
    MyClass.x; // 0 
    MyClass.printX(); // 0
    
  • 存取器方法
    • 存取器(accessor)是特殊的类方法,包括取值器(getter)和存值器(setter)两种方法
    • set方法的参数类型,必须兼容get方法的返回值类型,否则报错。
    • setget方法可访问性必须一致,都为公有或者私有
    class C {
        _name = '';
        get name() {
          return this._name;
        }
        set name(value) {
          this._name = value;
        }
      }
    
  • 类的 interface 接口
    • class使用 implements 关键字 来实现interfacetypeclass定义的类型
    interface Country { 
        name: string; capital: string; 
    } 
     // 或者 
    type Country = { 
        name: string; capital: string; 
    }; 
     
    class MyCountry implements Country { 
         name = ""; 
         capital = ""; 
    }
    
     class Car { 
         id: number = 1; 
         move(): void {} 
     } 
     class MyCar implements Car { 
         id = 2; // 不可省略 
         move(): void {} // 不可省略 
     }
    
    • 实现多个接口
     class Car implements MotorVehicle, Flyable, Swimmable {
         // ... 
     }
    
    • 不同接口不能有互相冲突的属性
  • class类型
    • 实例类型: 类名代表该类的实例类型,而不是 class 的自身类型
    • 类的自身类型:类的自身类型就是一个构造函数, 通过typeof获取
    class Color { 
        name: string; 
        constructor(name: string) { 
            this.name = name; 
        } 
    } 
    const green: Color = new Color("green");
    
    function createColor(ColorClass: typeof Color, x: string): Color {
        return new ColorClass(x); 
    }
    
    const red: Color = createColor(Color, 'red')
    
    • 也遵循结构类型原则: 确定两个类的关系时,只检查实例成员,不j检查静态成员和构造函数
  • 类的继承
    • 可以使用 extends 关键字继承另一个类(这里又称“基类”)的所有属性和方法。
    class A { 
        greet() { 
            console.log("Hello, world!"); 
        } 
    } 
    class B extends A {} 
    const b = new B(); 
    b.greet(); // "Hello, world!"
    
    • 子类也可以重写继承过来的方法和属性
      • 子类的同名方法不能与基类的类型定义相冲突。
      • 重写后可以通过super关键字调用基类的方法和属性
      • 子类可以改变继承下来的可访问性,改变protected为其它修饰符,但是不能改变为private
  • 可访问性修饰符
    • 类的内部成员(属性,方法,构造方法,静态成员)的外部可访问性,由三个可访问性修饰符(access modifiers)控制:publicprivateprotected
    • public修饰符表示这是公开成员,外部可以自由访问
    • private修饰符表示私有成员,只能用在当前类的内部,类的实例和子类都不能使用该成员。
      • 子类不能定义父类私有成员的同名成员
      • ES6 引入了自己的私有成员写法#propName。因此建议不使用private
    • protected修饰符表示该成员是保护成员,只能在类的内部使用该成员,实例无法使用该成员,但是子类内部可以使用。
      • 子类不仅可以拿到父类的保护成员,还可以定义同名成员。
    class A { 
        private x: number = 0; 
    } 
    const a = new A(); a.x; // 报错 
    class B extends A { 
        showX() { 
            console.log(this.x); // 报错 
        } 
    }
    
  • 抽象类、抽象成员
    • TypeScript 允许在类的定义前面,加上关键字abstract,表示该类不能被实例化,只能当作基类使用。这种类就叫做“抽象类”(abastract class)。
        abstract class A { id = 1; } 
        const a = new A(); // 报错
    
    • 抽象类只能当作基类使用,用来在它的基础上定义子类。
    • 抽象类可以继承其他抽象类。
    • 抽象成员:抽象类的内部未实现的属性和方法。叫做“抽象成员”
      • 属性名和方法名有abstract关键字,表示该方法需要子类实现。
      • 如果子类没有实现抽象成员,就会报错。
      • 抽象成员只能存在于抽象类,不能存在于普通类
      • 抽象成员不能有具体实现的代码
      • 抽象成员前也不能有private修饰符
  • this问题
    • 函数增加一个名为this的参数,放在参数列表的第一位,用来描述函数内部的this关键字的类型
    class A { 
        name = "A"; 
        getName(this: A) { 
            return this.name; 
        } 
    } 
    const a = new A(); 
    const b = a.getName; b(); // 报错
    
    • 在类的内部,this本身也可以当作类型使用,表示当前类的实例对象。
    class Box { 
        contents: string = ""; 
        set(value: string): this { 
            this.contents = value; 
            return this; 
        } 
    }
    

四.泛型

泛型就是将某些类型作为参数,在使用的时候传入,而不是一开始就定义好

参数要放在一对尖括号(<>)里面

例如我们常用的数组泛型

let stringArr: Array<string>
let numberArr: Array<number>

函数泛型的写法

    // 写法一
    function id<T>(arg: T): T { return arg; }
    // 写法二 
    let myId: <T>(arg: T) => T = id; 
    // 写法三 
    let myId2: { <T>(arg: T): T } = id;

interface泛型的写法

    interface Box<Type> {
      contents: Type;
    }

    let box: Box<string>;

类的泛型写法

class Pair<K, V> {
  key: K;
  value: V;
  constructor(key: K, value: V) { 
     this.key = key; 
     this.value = value
  } 
}
const a = new Pair<string, string>()
  • 继承泛型类时,必须给出类型
class A<T> { value: T; } 
class B extends A<any> {}
  • 静态属性不能使用泛型类

类型别名的泛型写法

type Container<T> = { value: T }; 
const a: Container<number> = { value: 0 }; 
const b: Container<string> = { value: "b" };

递归嵌套

type Tree<T> = { 
    value: T; 
    left: Tree<T> | null; 
    right: Tree<T> | null; 
};

默认类型

使用时,如果泛型没有给出类型参数的值,就会使用默认值

function getFirst<T = string>(arr: T[]): T {
  return arr[0];
}

类型参数的约束条件

使用<TypeParameter extends ConstraintType>对泛型进行约束,不满足时会报错

function comp<T extends { length: number }>(a: T, b: T) {
    if (a.length >= b.length) { return a; } 
    return b; 
}

泛型参数可以使用其他泛型作为约束 <T, U extends T>

五.类型运算

1.交叉

交叉类型(intersection types)指的多个类型组成的一个新类型,使用符号&表示。

  • 交叉类型的主要用途是表示对象的合成
let obj: { foo: string } & { bar: string };

obj = {
  foo: "hello",
  bar: "world",
};
  • 当交叉类型由互斥的基本类型组成时,ts会认为实际类型是never
let x: number & string; // never
  • 当有关联的两个类组成交叉类型时,返回子类型
type A = string & 'h'  //'h'
  • 任何值与known相交都返回其本身(known可以看作所有类型的父类)
type T = unknown & null; // null
  • T & {},除了undefined和null,其它类型都属于{}子类型,所以相交也返回子类型,而undefined和null则返回never

2.联合

联合类型(union types)指的是多个类型组成的一个新类型,使用符号|表示。

联合类型A|B表示,任何一个类型只要属于AB,就属于联合类型A|B

let x: string | number; 
x = 123; // 正确 
x = "abc"; // 正确

前面提到,打开编译选项strictNullChecks后,其他类型的变量不能赋值为undefinednull。这时,如果某个变量确实可能包含空值,就可以采用联合类型的写法。

let name: string | null; 
name = "John"; 
name = null;

3.类型缩小

如果一个变量可能有多个类型,那么在使用它时需要进行类型缩小。 通常需要结合if语句或者switch语句。当然最基本的赋值缩小不需要结合判断语句

  • 赋值缩小
let x: string | number
x = 'hello' 
x.toFixed(111) // 报错

x = 123; 
x.toFixed(111) // 正确
  • 通过typeof缩小
function printId(id: number | string) { 
    if (typeof id === "string") {
        console.log(id.toUpperCase()); 
    } else { 
        console.log(id); 
    } 
}
  • 通过instance缩小
function f(x: Date | RegExp) { 
    if (x instanceof Date) { x; // Date } 
    if (x instanceof RegExp) { x; // RegExp } 
}
  • 通过in 缩小
interface A { x: number; } 
interface B { y: string; } 
function f(x: A | B) { 
    if ("x" in x) { x; // A } 
    else { x; // B } 
}
  • 通过特征属性缩小
interface UploadEvent {
  type: "upload";
  filename: string;
  contents: string;
}
interface DownloadEvent {
  type: "download";
  filename: string;
}
type AppEvent = UploadEvent | DownloadEvent;

function handleEvent(e: AppEvent) {
  switch (e.type) {
    case "download":
      e; // Type is DownloadEvent
      break;
    case "upload":
      e; // Type is UploadEvent
      break;
  }
}
  • 通过is进行类型保护
function isInputElement(el: HTMLElement): el is HTMLInputElement {
  return "value" in el;
}

function getElementContent(el: HTMLElement) {
  if (isInputElement(el)) {
    el; // Type is HTMLInputElement
    return el.value;
  }
  el; // Type is HTMLElement
  return el.textContent;
}

4.类型兼容

结构类型原则

ts 的一个规则是,凡是可以使用对象A的地方,都可以使用对象B,那么类型A就称为类型B的子类型(subtype)。但是子类型不能使用父类型。

type RootType = {
    a: string
}

type ChildType {
    a: string
    b: string
}

const child: ChildType = {
    a: 'a',
    b: 'b'
}
const root: RootType = child;
    
const a: 'hello' = 'hello'
const b: string = a
  • 所以对于顶层类型any,我们可以理解为它是所有类型的子类型,因为any类型的变量可以赋予其它变量。它同时也是所有类型的父类型,因为any类型的变量可以被赋予任意类型的值
let a: any
a = 123

let b: string
b = a
  • 同理对于底层类型never,是所有类型的子类型
let a: never
a = 123 // 报错

let b: string
b = a // 不报错
  • 对于unknown,是其它所有类型的父类型,只是any的子类型

子类型和父类型的概念可以方便我们理解常见的类型运算,比如赋值, &, 与 extends

type T30<T> = unknown extends T ? true : false;
type T31 = T30<string> // false

type T32<T> = T extends unknown ? true : false;
type T33 = T32<string> // true

对于有关联的联合类型,我们也可以引入父类子类概念

例如:1|2 可以看成 1|2|3的子类

type T0<T> = [T] extends [1|2] ? true : false
type T1 = T0<1> // true
type T2 = T0<1|2|3> // false

运算律

  • 改变成员类型的顺序不影响联合类型的结果类型。
type T0 = string | number;
type T1 = number | string;

优先级

  • &的优先级高于|
    • A | B & C 等于 A | (B & C)

分配律

  • (A | B) & (C | D) ≡ A & C | A & D | B & C | B & D

    T = (string | 0) & (number | "a");
    T = (string & number) | (string & "a") | (0 & number) | (0 & "a");
    T = never | "a" | 0 | never;
    T = "a" | 0;
    

六.类型运算符

1.typeof运算符

  • TypeScript 将typeof运算符移植到了类型运算,它的操作数依然是一个值,但是返回的不是字符串,而是该值的 TypeScript 类型。
const a = { x: 0 };
type T0 = typeof a; // { x: number }
type T1 = typeof a.x; // number
  • typeof 的参数只能是标识符,不能是需要运算的表达式
type T = typeof Date(); // 报错
  • typeof命令的参数不能是类型。
type Age = number;
type MyAge = typeof Age; // 报错

2.keyof运算符

  • 返回对象所有键名组成的联合类型
type MyObj = {
  foo: number;
  bar: string;
};

type Keys = keyof MyObj; // 'foo'|'bar'                          
  • 对于any
// string | number | symbol
type KeyT = keyof any;
  • 对象有属性索引,则返回索引类型
interface T {
  [prop: number]: number;
}
// number
type KeyT = keyof T;
  • 对于数组或元祖
type Result = keyof ["a", "b", "c"];
// 返回 number | "0" | "1" | "2"
// | "length" | "pop" | "push" | ···
  • 对于联合类型,keyof 返回成员共有的键名
type A = { a: string; z: boolean };
type B = { b: string; z: boolean };
// 返回 'z'
type KeyT = keyof (A | B);
  • 对于交叉类型,keyof 返回所有键名 keyof (A & B) ≡ keyof A | keyof B

3.readonly

readonly设为只读, -readonly表示去除只读

 type Mutable<Obj> = {
  -readonly [Prop in keyof Obj]: Obj[Prop];
};
// 用法
type MyObj = {
  readonly foo: number;
};
// 等于 { foo: number; }
type NewObj = Mutable<MyObj>;                           

4.?

?变为可选,-? 去除可选

type Concrete<Obj> = {
  [Prop in keyof Obj]-?: Obj[Prop];
};

// 用法
type MyObj = {
  foo?: number;
};

// 等于 { foo: number; }
type NewObj = Concrete<MyObj>;

5.in

遍历联合类型中的每一个成员

type U = "a" | "b" | "c";

type Foo = {
  [Prop in U]: number;
};
// 等同于
type Foo = {
  a: number;
  b: number;
  c: number;
};

6.方括号运算符

取出对象属性的类型

type Person = { age: number; name: string; alive: boolean; }; 
// Age 的类型是 number 
type Age = Person["age"];
  • 如果该属性不存在,则报错
  • 方括号的参数如果是联合类型,那么返回的也是联合类型。
  • 也可以取出索引属性的类型
type Obj = { [key: string]: number; }; 
// number 
type T = Obj[string];

7.extends

条件运算符,语法为 T extends U ? X : Y

类型T是否可以赋值给类型U,即T是否为U的子类型,这里的TU可以是任意类型。

  • 对于联合类型
(A|B) extends U ? X : Y
// 等同于
(A extends U ? X : Y) |
(B extends U ? X : Y)
  • 如果只是不希望展开,可以将两侧类型都放到方括号里
// 示例一
type ToArray<Type> = Type extends any ? Type[] : never;
// string[]|number[]
type T = ToArray<string | number>;

// 示例二
type ToArray<Type> = [Type] extends [any] ? Type[] : never;
// (string | number)[]
type T = ToArray<string | number>;

8.infer

它通常跟条件运算符一起使用,用在extends关键字后面的父类型之中。用来提取左边子类型的一部分而不是全部

// 提取数组类型
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
type Str = Flatten<string[]>; // string 

// 提取函数参数及返回值
type ReturnPromise<T> = T extends (...args: infer A) => infer R
  ? (...args: A) => Promise<R>
  : T;

// 提取对象属性类型
type MyType<T> = T extends {
  a: infer M;
  b: infer N;
}
  ? [M, N]
  : never;

// 提取字符串
type Str = "foo-bar";
type Bar = Str extends `foo-${infer rest}` ? rest : never; // 'bar'

9.is

函数返回布尔值的时候,可以使用is运算符,限定返回值与参数之间的关系。写法采用parameterName is Type的形式

function isFish(pet: Fish | Bird): pet is Fish { 
    return (pet as Fish).swim !== undefined; 
}
  • is可以用于类型保护
function isCat(a: any): a is Cat { 
    return a.name === "kitty"; 
} 
let x: Cat | Dog; 
if (isCat(x)) { 
    x.meow(); // 正确,因为 x 肯定是 Cat 类型 
}

10.模板字符串

TypeScript 允许使用模板字符串,构建类型。 模板字符串的最大特点,就是内部可以引用其他类型

type World = "world"; // "hello world" 
type Greeting = `hello ${World}`;
  • 模板字符串可以引用的类型一共 6 种,分别是 string、number、bigint、boolean、null、undefined

七.断言

  • expr as T

类型断言要求实际的类型与断言的类型兼容,实际类型可以断言为一个更加宽泛的类型(父类型),也可以断言为一个更加精确的类型(子类型),但不能断言为一个完全无关的类型。

  • expr as unknown as T

如果真的要断言成一个完全无关的类型,也是可以做到的。那就是连续进行两次类型断言,先断言成 unknown 类型或 any 类型,然后再断言为目标类型

  • as const断言
    • as const会将字面量的类型断言为不可变类型,缩小成 TypeScript 允许的最小类型。
    • 只能用于字面量,不能用于变量
    // a1 的类型推断为 number[]
    const a1 = [1, 2, 3];
    // a2 的类型推断为 readonly [1, 2, 3]
    const a2 = [1, 2, 3] as const;
    
  • 非空断言
    • 对于那些可能为空的变量(即可能等于undefined或null),TypeScript 提供了非空断言,保证这些变量不会为空,写法是在变量名后面加上感叹号!
    • 非空断言只有在打开编译选项strictNullChecks时才有意义
  • 断言函数
    • 语法:用asserts函数参数进行类型判断
    • asserts语句等同于void类型,所以如果返回除了undefined和null以外的值,都会报错
    function isString(value:unknown):asserts value is string {
      if (typeof value !== 'number')
        throw new Error('Not a number');
    }
    
    function assert(x:unknown):asserts x {
      if (!x) {
        throw new Error(`${x} should be a truthy value.`);
      }
    }
    
    • 断言函数类型保护函数(type guard)是两种不同的函数。它们的区别是,断言函数不返回值,而类型保护函数总是返回一个布尔值
    function isString(
      value:unknown
    ):value is string {
      return typeof value === 'string';
    }
    

八.类型工具

ts内置了很多种类型工具,能够直接使用

1.Awaited<T>

  • 取出promise的返回值
  • 如果不是promise,则返回原值
// number | boolean
type C = Awaited<boolean | Promise<number>>;

底层实现

type Awaited<T> =
    T extends null | undefined ? T : // special case for `null | undefined`
                                     // when not in `--strictNullChecks` mode
        T extends object & { then(onfulfilled: infer F, ...args: infer _): any } ?
        // `await` only unwraps object types with a callable `then`.
        // Non-object types are not unwrapped
            F extends ((value: infer V, ...args: infer _) => any) ?
            // if the argument to `then` is callable, extracts the first argument
                Awaited<V> : // recursively unwrap the value
                never : // the argument to `then` was not callable
        T; // non-object or non-thenable

2.ReturnType<Type>

提取函数类型的返回值类型

type T1 = ReturnType<() => string>; // string

实现

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

3.Parameters<Type>

提取函数参数类型, 返回一个元祖类型

type T2 = Parameters<(s: string) => void>; // [s:string]

实现

type Parameters<T extends (...args: any) => any> = T extends (
  ...args: infer P
) => any
  ? P
  : never;

4.ThisParameterType<Type>

提取函数类型中this参数的类型

function toHex(this: Number) {
  return this.toString(16);
}
type T = ThisParameterType<typeof toHex>; // number

实现

type ThisParameterType<T> =
  T extends (
    this: infer U,
    ...args: never
  ) => any ? U : unknown

5.InstanceType<Type>

提取构造函数的返回值的类型(即实例类型)

type T = InstanceType<new () => object>; // object

class C {
  x = 0;
  y = 0;
}
type T = InstanceType<typeof C>; // C

实现

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

6.ConstructorParameters<Type>

提取构造函数的参数,返回一个元祖

type T2 = ConstructorParameters<new (x?: string) => object>; 
// [x?: string | undefined]

any类型和never类型是两个特殊值,分别返回unknown[]和never

实现

type ConstructorParameters<T extends abstract new (...args: any) => any> =
  T extends abstract new (...args: infer P) => any ? P : never;

7.ThisType<Type>

不返回类型,只用来跟其他类型组成交叉类型,用来提示 TypeScript 其他类型里面的this的类型。注意,使用这个类型工具时,必须打开noImplicitThis设置

interface HelperThisValue {
  logError: (error: string) => void;
}
let helperFunctions: { [name: string]: Function } & ThisType<HelperThisValue> =
  {
    hello: function () {
      this.logError("Error: Something wrong!"); // 正确
      this.update(); // 报错
    },
  };

8.OmitThisParameter<Type>

从函数类型中移除 this 参数

function toHex(this: Number) {
  return this.toString(16);
}

type T = OmitThisParameter<typeof toHex>; // () => string

实现

type OmitThisParameter<T> = unknown extends ThisParameterType<T>
  ? T
  : T extends (...args: infer A) => infer R
  ? (...args: A) => R
  : T;

9.Exclude<UnionType, ExcludedMembers>

用来从联合类型UnionType里面,删除某些类型ExcludedMembers,组成一个新的类型返回

type T = Exclude<number | boolean, boolean>; // number
type T2 = Exclude<number | boolean, boolean | number>; // never

实现

type Exclude<T, U> = T extends U ? never : T;

10.Extract<Type, Union>

用来从联合类型UnionType之中,提取指定类型Union,组成一个新类型返回。它与Exclude<T, U>正好相反

type T6 = Extract<200 | 400, 200 | 201>; // 200

实现

type Extract<T, U> = T extends U ? T : never;

11.Pick<Type, Keys>

返回一个新的对象类型,第一个参数Type是一个对象类型,第二个参数Keys是Type里面被选定的键名

interface A {
  x: number;
  y: number;
}

type T1 = Pick<A, "x">; // { x: number }

实现

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

12.Omit<Type, Keys>

用来从对象类型Type中,删除指定的属性Keys,组成一个新的对象类型返回。

interface A {
  x: number;
  y: number;
}

type T1 = Omit<A, "x">; // { y: number }

实现

type Omit<T, U> = Pick<T, Exclude<keyof T, U>>

13.NonNullable<Type>

用来从联合类型Type删除null类型和undefined类型,组成一个新类型返回,也就是返回Type的非空类型版本

    type NonNullable<T> = T & {};
    type T = NonNullable<string | null> //string

T & {}等同于求T & Object的交叉类型。由于 TypeScript 的非空值都属于Object的子类型,所以会返回自身;而null和undefined不属于Object,会返回never类型。

14.Readonly<Type>

返回一个新类型,将参数类型Type的所有属性变为只读属性 type Readonly = {   readonly [P in keyof T]: T[P]; };

15.ReadonlyArray<Type>

返回一个只读数组

interface ReadonlyArray<T> {
  readonly length: number;
  readonly [n: number]: T;
  // ...
}

16.Partial<Type>

返回一个新类型,将参数类型Type的所有属性变为可选属性

type Partial<T> = {
  [P in keyof T]?: T[P];
};

返回一个新类型,将参数类型Type的所有属性变为可选属性

17.Required<Type>

返回一个新类型,将参数类型Type的所有属性变为必选属性 type Required = {   [P in keyof T]-?: T[P]; };

18.Record<Keys, Type>

返回一个对象类型,参数Keys用作键名,参数Type用作键值类型

// { a: number, b: number }
type T = Record<"a" | "b", number>;

实现

type Record<K extends string | number | symbol, T> = { [P in K]: T };

19.Uncapitalize<StringType>

将字符串的第一个字符转为小写。

type A = "HELLO";
// "hELLO"
type B = Uncapitalize<A>;

20.Capitalize<StringType>

将字符串的第一个字符转为大写

21.Uppercase<StringType>

将字符串类型的每个字符转为大写。

22.Lowercase<StringType>

将字符串的每个字符转为小写

未完待续

类型映射

装饰器

模块

namespace

declare

.d.ts

tsconfig

参考

TypeScript 泛型 | 阮一峰 TypeScript 教程 (p6p.net)