TypeScript基础知识

118 阅读12分钟

本文总结了TypeScript的基础知识,包括内置类型、类型结合、操作符、实用类型、高级类型等等......

内置类型

元组tuple

元组是指定数量的数组,数组内的元素可以是不同类型的

type NameAndAge = [string, number]

const nameAndAge: NameAndAge = ['qq', 18]

可以配合解构符号一起使用

const nameAndAge:[string, number] = ['qq', 18]
const [name, age] = nameAndAge
// qq
console.log(name)

枚举类型enum

  1. 数字枚举
enum Direction {
  Up = 1,
  Down = 2,
  Left = 3,
  Right = 4
}

不显式声明枚举值的情况下,第一个枚举值会被赋值为0,其余的枚举值会根据顺序进行自增

enum Direction {
  Up,
  Down,
  Left,
  Right
}
// 等同于
enum Direction {
  Up = 0,
  Down = 1,
  Left = 2,
  Right = 3
}
  1. 字符串枚举

由于字符串没有自增长的行为,因此必须显式声明一个字符串常量作为枚举值

enum Direction {
  Up = 'up',
  Down = 'down',
  Left = 'left',
  Right = 'right'
}

枚举类型的常用实践可以移步:TypeScript开发实践:枚举类型

any和unknown

any:我不在乎它是什么类型

unknown:我不知道它是什么类型

对于声明为any类型的变量,可以执行任何操作(访问属性、调用函数等等),ts不会报错

let value: any;

value.foo.bar; // OK
value.trim(); // OK
value(); // OK
new value(); // OK
value[0][1]; // OK

而对于声明为unknown类型的变量,对其执行操作之前,必须先进行类型检查或者类型断言

function invokeCallback(callback: unknown) {
    try {
        // Object is of type 'unknown'.(2571)
        callback(); // Error
    } catch (err) {
        console.error(err)
    }
}

可以把unknown看作“安全的any”

解决上述报错的方法是:提前对callback进行类型检查

具体写法就是用typeof 或者 instanceof进行类型判断

function invokeCallback(callback: unknown) {
    try {
        if (typeof callback === 'function') {
          callback()
         }
    } catch (err) {
        console.error(err)
    }
}

函数

函数类型声明

使用type声明一个函数

type F = (a: number, b: string) => boolean
// 等同于
type F = {
  (a: number, b: string): boolean
}

在interface里面声明一个函数

interface Something {
  f: (a: number, b: string) => boolean
}

函数重载

如果函数入参的数量或类型是不确定的,可以通过多次声明函数头来进行重载

例如:在给CSS属性padding传值的时候,可以有4种传值方式

  • 只传一个数all:表示上下左右的左右padding都是all
  • 传两个数topAndBottom、leftAndRight:表示上下padding是topAndBottom,左右padding是leftAndRight
  • 传三个数top、leftAndRight、bottom:表示上padding是top,下padding是bottom,左右padding是leftAndRight
  • 传四个数:分别指定上右下左的padding
interface Padding {
  top: number,
  bottom: number,
  left: number,
  right: number
}

function padding(all: number): Padding
function padding(topAndBottom: number, leftAndRight: number): Padding
function padding(top: number, leftAndRight: number, bottom: number): Padding
function padding(top: number, right: number, bottom: number, left: number): Padding
function padding(a: number, b?:number, c?:number, d?:number): Padding {
  if (b === undefined && c === undefined && d === undefined) {
    b=c=d=a
  } else if (c === undefined && d ===undefined) {
    c=a
    d=b
  }

  return {
    top: a,
    right: b,
    bottom: c,
    left: d
  } as Padding
}

实际编写项目代码时,上述写法会被@typescript-eslint/unified-signatures提示报错

因为多个函数声明理论上都可以通过联合类型或者可选参数来合并成一个,从而避免冗余代码

泛型

泛型是类型声明里面的变量,在实际运行的时候才能确定

例如:定义一个函数类型,它的返回值类型一定等同于它的第一个入参的类型

type LogFunc<T> = (arg: T) => T

例如:我们希望泛型T必须包含length属性

可以用extends来继承一个只包含length属性的类型

interface Lengthwise {
  length: number
}

type LogFunc<T extends Lengthwise> = (arg: T) => T

这样一来我们就可以在函数体里面读取第一个入参的length属性

const log:LogFunc<number[]> = (array) => {
  console.log(array.length) //ok
  return array
}

例如:我们希望泛型K必须是另外一个泛型T的属性之一

function f<T, K extends keyof T>(obj:T, key: K) {
  return obj[key]
}

let obj = {
  name: 'qq',
  age: 18
}

// Ok
f(obj, 'name')

// Error: Argument of type '"height"' is not assignable to parameter of type '"name" | "age"'
f(obj, 'height') 

类型断言

TypeScript允许你覆盖它的推断,最常见的就是使用as关键字

也可以使用!后缀来去除null或者undefined

function f(name: string | undefined) {
  const lowerCaseName = name!.toLowerCase()
}

类型结合

交叉类型

如果你想基于两个原对象创建一个新对象,新对象同时拥有2个原对象所有的属性,就可以使用交叉类型

  • 想用type声明新对象,用关键字&
  • 想用interface声明新对象,用关键字extends
interface Point {
  x: number
  y: number
}

interface Named {
  name: string
}

type NewPoint = Point & Named
// 那么等同于
// interface NewPoint {
//   x: number;
//   y: number;
//   name: string;
// }


// 类似地,我们也可以这样写
interface NewPoint extends Point {
  name: string
}

// extends继承多个
interface NewType extends Point, Named {
  age: number
}
// 那么等同于
// interface NewType {
//   x: number
//   y: number
//   name: string
//   age: number
// }

交叉运算符&的特性

  • 唯一性:A & A 等价于A
  • 满足交换律: A & B 等价于 B & A
  • 满足结合律:A & ( B & C )等价于 ( A & B ) & C
  • 父类型收敛:如果B是A的父类型,则A & B会被收敛成A(相当于一个小圆,完全在一个大圆的范围内)
  • 除了never类型外,任何类型与any进行交叉运算的结果都是any

下面看运算的例子:

type A0 = 1 & number; // 1
type A1 = '1' & string; // '1'
type A2 = any & 1; // any
type A3 = any & never; // never
type A4 = string & number // never

在对 对象类型的type使用交叉运算符的时候,如果他们包含相同的属性,但是属性类型又不一样

那么就会对具体的2个重名属性,进行交叉运算,生成新的属性类型

例如:重名属性是不同的基本数据类型

interface X {
  c: string;
  d: string;
}

interface Y {
  c: number;
  e: string
}

type XY = X & Y
// 此时生成的新类型XY中,c属性的类型是never,因为 string & number的结果是never

例如:重名属性是不同的对象类型

interface D { d: boolean; }
interface E { e: string; }
interface F { f: number; }

interface A { x: D; }
interface B { x: E; }
interface C { x: F; }

type ABC = A & B & C;

let abc: ABC = { // Ok
    x: {
      d: true,
      e: '阿宝哥',
      f: 666
    }
};

联合类型

如果你想声明一个新类型,它的实际取值为多种类型之一,可以使用|声明联合类型

// 要么是字符串,要么是字符串数组
type Members = string | string[]

操作符

instanceof

instanceof用于检测右侧构造函数的prototype属性,是否出现在左侧实例对象的原型链上

  • 左侧:一个实例对象

  • 右侧:一个构造函数

keyof

keyof接受一个对象类型作为参数,返回该对象的所有属性名对应的字面量组成的联合类型

interface Person{
  name: string
  age: number
}

type PersonKey = keyof Person
// 等同于type PersonKey = 'name' | 'age'

typeof

一般情况下,我们会在if判断语句里面,用typeof来进行类型缩窄

function SaySomething(str: string | string[]) {
  if(typeof str === 'string') {
    // 在这里才可以安全的使用string原型上的方法
  }
}

其他用途:

例如:获取枚举变量的所有属性名

enum HttpMethod {
    get,
    post,
    del
}

type Method1 = keyof typeof HttpMethod
// 类型 Method1与Method2 完全相同
type Method2 = 'get' | 'post' | 'del'

例如:获取函数类型

获取函数类型之后,可以利用ReturnType Parameters 来获取函数的返回类型和参数类型

function add(a: number, b: number) {
  return a + b;
}

type AddType = typeof add; // (a: number, b: number) => number
type AddReturnType = ReturnType<AddType> // number
type AddParamsType = Parameters<AddType> // [a: number, b: number]

in

in操作符可以用于检查一个对象上是否存在某个属性

interface A {
 x: number
}

interface B {
  y: string
}

function f(q: A | B) {
  if ('x' in q) {
    // ts会自动推断q的类型是A
  } else {
    // ts会自动推断q的类型是B
  }
}

type和interface的对比

相同点

type叫做“类型别名”,interface叫做“接口”

两者都可以用来定义一个对象或者函数

// 定义一个对象
interface Peron {
  name: string;
  age: number;
}

type Person = {
  name: string;
  age: number;
}

// 定义一个函数
interface addType {
  (num: number): number;
}

type addType = (num: number) => number;

两者都允许继承:

用interface构造新类型实现继承,用extends

// interface 继承 interface
interface PersonA { 
  name: string 
}
interface StudentA extends Person { 
  grade: number 
}

// interface 继承 type
type PersonB = { 
  name: string 
}

interface StudentB extends PersonB { 
  grade: number 
}

用于type构造新类型实现继承,用交叉类型&

// type 继承 type
type PersonA = {
  name: string
}
type StudentA = PersonA & { grade: number }

// type 继承 interface
interface PersonB { 
  name: string 
}

type StudentB = PersonB & { grade: number  }

不同点

type可以,但是interface不行的

  • 声明基本类型
  • 声明联合类型 |
  • 声明交叉类型 &
  • 声明元组
type Name = string                              

type arrItem = number | string                  

type Student = Person & { grade: number }        

type StudentAndTeacherList = [Student, Teacher] 

interface可以,但是type不行

  • 合并重复声明(type会报错)
interface Person {
    name: string
}

interface Person { 
    age: number
}

const person: Person = {
    name: 'lin',
    age: 18
}

实用类型

Partial

Partial<T>把类型T的所有属性都变成可选的,构成一个新的类型

Pick

Pick<T, key1 | key2>从类型T中选取一个或多个属性,构成一个新的类型

Omit

Omit<T, key1 | key2>从类型T中剔除一个或多个属性,构成一个新的类型

interface Person = {
  name: string;
  age: string;
  location: string;
}

type PersonWithoutLocation = Omit<Person, 'location’>
// 等同于
type QPerson = {
  name: string;
  age: string;
};

Record

Record<K, T>用于构造一个新的对象类型,它的属性取值为K,每个属性的类型都是T

常见的用途:以枚举类型为基础,映射出每个枚举值对应的一个特性

enum MemberType {
  User = 0,
  Department = 1,
  ServiceAccount = 2
}

const MapTypeToName:Record<MemberType, string> = {
  [MemberType.User]: '个人',
  [MemberType.Department]: '部门',
  [MemberType.ServiceAccount]: '服务帐号'
}

Readonly

Readonly<T>用于将T的所有属性都变成只读的,从而使其不能被重新赋值

interface Todo { 
  title: string
}
const todo: Readonly<Todo> = { 
  title: 'say hi'
};
// Cannot assign to 'title' because it is a read-only property.todo.title = 'Hello';
todo.title = 'hello'

Required

Required<T>用于将T的所有属性都变成必填的

interface Person {
  name: string
  age?: number
  other?: {
    height: number
    weight?: number
  }
}

type NewPerson = Required<Person>
// 相当于
type NewPerson = {
  name: string
  age: number
  other: {
    height: number
    weight?: number
  }
}

Exclude

Exclude<UnionType, K>用于从某个联合类型中,剔除 可分配(assign)给K的的联合成员

type T0 = Exclude<"a" | "b" | "c", "a">;
//  type T0 = "b" | "c"

type T1 = Exclude<"a" | "b" | "c", "a" | "b">;
//  type T1 = "c"

type T2 = Exclude<string | number | (() => void), Function>;
//  type T2 = string | number

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; x: number }
  | { kind: "triangle"; x: number; y: number };

type T3 = Exclude<Shape, { kind: "circle" }>
//  type T3 = { kind: "square"; x: number } | { kind: "triangle"; x: number; y: number }

Extract

Exclude和Extract的关系,与Omit和Pick的关系是类似的

Extract<UnionType, K>用于从某个联合类型中,选取 可分配(assign)给K的联合成员

type T0 = Extract<"a" | "b" | "c", "a">;
//  type T0 = "a"

type T1 = Extract<"a" | "b" | "c", "a" | "b">;
//  type T1 = "a" | "b"

type T2 = Extract<string | number | (() => void), Function>;
//  type T2 = (() => void)

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; x: number }
  | { kind: "triangle"; x: number; y: number };

type T3 = Extract<Shape, { kind: "circle" }>
//  type T3 = { kind: "circle"; radius: number }

NonNullable

NonNullable<T | K>用于从某个联合类型中,剔除nullundefined

type T0 = NonNullable<string | number | undefined>
// type t0 = string | number

它的源码是这样的

type NonNullable<T> = T & {}

类型{}包括了除nullundefined之外的所有类型,再取其与泛型T的交叉类型,最终达到过滤nullundefined的目的

Parameters

Parameters基于一个函数类型来构造一个元组,元组的元素和类型对应此函数的入参

type T0 = Parameters<() => string>;
// type T0 = []

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

interface Person {
  a: number
  b: string
}

type T2 = Parameters<(person: Person) => void>
// type T2 = [person: Person]

用途:获取一个函数的参数类型

function f(a: number, b: string) {
  if (a >=0) {
    return b
  }
  return b+'666'
}

type P = Parameters<typeof f>
// type P = [a: number, b: string]

ReturnType

ReturnType用于获取一个函数的返回类型

function f(a: number, b: string): string {
  if (a >=0) {
    return b
  }
  return b+'666'
}

type T0 = ReturnType<typeof f>
// type T0 = string

Awaited

Awaited用于模拟async函数中的await操作,以获取promise的返回类型

type A = Awaited<Promise<string>>;
// type A = string

type B = Awaited<Promise<Promise<number>>>;
// type B = number

高级类型

字面量

字面量类型用于强制指定特定的值,它可以分为字面量基本类型字面量对象类型

字面量基本类型可以指定一个numberstring或者boolean

type Age = 18

let age1: Age = 18 // Ok
let age2: Age = 20 // Error

字面量对象类型

interface Info {
  count: 0
}

// Error: Type '10' is not assignable to type '0'
let info: Info = {
  count: 1
}

模板字面量

模板字面量是基于一个给定的字符串 字面量类型进行拓展的新类型,语法与javascript的模板字符串类似

type World = 'world'

type Greeting = `hello ${world}`
// 等同于 type Greeting = 'hello world'

type Name = 'Mr' + string

let name1: Name = 'Mr Mike' // Ok
let name2: Name = 'March' // Error

type UsLocaleIDs = 'enUs' | 'en'
type ZhLocaleIDs = 'zhCn' | 'zh'
type LocaleIDs = `${UsLocaleIDs | ZhLocaleIDs}_ids`
// 等同于 type LocaleIDs = 'enUs_ids' | 'en_ids' | 'zhCn_ids' | 'zh_ids'

映射类型

映射类型是一种泛型类型,它通过遍历一个对象类型K的所有属性,来构建一个新的类型

type Mapping<K> = {
  [P in keyof K]: T;
}

需要注意的是:K必须是string、number、symbol中的一种

修饰符:

  • readonly:添加在在中括号前面
  • ?: 可选符,添加在冒号前面
  • 横杠: 移除符,添加在需要移除的修饰符前面
// 将属性P变为可选的
[P in K]?: T
// 移除属性P原来的可选符
[P in K]-?: T
// 将属性P变为只读的
readonly [P in K]: T
// 移除属性P的只读符
-readonly[P in K]: T

用途1:修改对象类型里面每个属性的特性(是否为readonly、是否为可选)

例如:把类型User里面的所有属性都改成可选的(更简单的办法是使用Partial)

type User = {
  name: string;
  address: string;
}

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

type PartialUser = MyPartial<User>

用途2:修改对象类型里面每个属性的类型

例如:把类型User里面的所有属性的类型都改成一个函数类型,返回值为原来的类型

type User = {
  name: string;
  address: number;
}

type MapUserFunction<T> = {
  [P in keyof T]: () => T[P]
}

type UserFunction = MapUserFunction<User>
// 等同于
type UserFunction = {
  name: () => string
  address: () => number
}

用途3:修改对象类型里面每个属性的名称

例如:对于类型User里面的所有属性,为其名称加上User前缀

type User = {
  name: string;
  address: number;
}

type MapUserInfo<T> = {
  [P in keyof T as `User${P}`]: T[P]
}

type UserInfo = MapUserInfo<User>
// 等同于
type UserInfo = {
  Username: string
  Useraddress: number
}

递归类型

在类型定义中引用自身的类型,就是递归类型

例如:定义一个链表类型,每个节点都包含一个next属性指向下一个节点

type LinkedList<T> = {
  value: T,
  next: LinkedList<T> | null
}

let list:LinkedList<number> = {
  value: 1,
  next: {
    value: 2,
    next: null
  }
}

例如:定义一个树形结构,每个节点都包含一个children属性指向子节点

type Tree<T> = {
  value: T
  children: Array<Tree<T>>
}

let tree:Tree<number> = {
  value: 1,
  children: [
    {
      value: 2,
      children: []
    },
    {
      value: 3,
      children: []
    }
  ]
}

索引类型

我们可以在类型声明中,使用中括号[]来读取其他类型的某个指定的属性

type Person = {
  age: number
  name: string
  address: string
}

type T0 = Person['name']
// type T0 = string

type T2 = Person['age' | 'name']
// type T2 = number | string

用途1:通过keyof获取某个对象类型的所有值的联合类型

type Person = {
  age: number
  name: string
  isAdult: boolean
}

type T3 = Person[keyof Person]
// type T3 = number | string | boolean

用途2:通过[number]获取某个数组的元素值的联合类型

const a = [
  {
    name: 'q',
    age: 1
  },
  {
    name: 'X',
    age: 1
  }
]

type A = typeof a[number]
// type A = { name: string, age: number }

参考资料

TypeScript官方文档:

TypeScript知识地图:roadmap.sh/typescript

《深入理解TypeScript》中文翻译版:jkchao.github.io/typescript-…