TypeScript入门

1,098 阅读32分钟

什么是TypeScript

TypeScript是JavaScript的一个超集,主要提供了 系统类型和对ES6+的支持,让JS变成强类型语言。

typeScript的官方云环境

基础数据类型

  • string
  • number
  • boolean
  • undefined
  • null
  • symbol
  • BigInt
  • Object

null 和 undefined是所有类型的子类型,因此可以把 nullundefined赋值给其他类型;

number 和 bigint都表示数字,但是这两个类型不能互相赋值,不兼容;

其他类型

数组

一般定义方式:

let arr: string[] = ['name', 'age', 'sex'];
let arr: Array<string> = ['name', 'age', 'sex'];

定义对象成员的数组:

let arr1: { label: string; value: string; }[] = [{label: '姓名', value: 'name'}];

// 等价于

interface ArrType {
  label: string;
  value: string;
}
let arr2: ArrType[] = [{label: '姓名', value: 'name'}];

// 等价于

let arr3: Array<ArrType> = [{label: '姓名', value: 'name'}];

类数组

类数组不是数组类型,比如像 arguments的定义:

function fun () {
  let args: {
    [index: number]: number;
    length: number;
     callee: Function;
  } = arguments;
}

// 等价于

function fun1 () {
  let args: IArguments = arguments;
}

常用的类数组都有自己的接口定义,比如上述的arguments就有内置类型IArguments;除此之外,还有NodeList,HTMLCollection等。

函数

函数声明:

function fun (x: string; y: number): boolean {
  return true;
}

函数表达式:

const fun: (x: string; y: number) => boolean = function (x: number, y: number): boolean {
  return false;
}

用接口定义函数类型:

interface Fun {
  (): boolean;
}

// 使用
const fun: Fun = function () {
  return true;
}

tips: 这种方式定义函数类型时,不允许设置参数默认值。

可选参数:

function fun (x: string; y?: number): boolean {
  return false;
}

tips: 可选参数后面不允许在出现必须参数。

参数默认值:

function fun (x: string = 'hello') {}

剩余参数:

function fun (x: string, ...rest: any[]) {}

函数重载:

函数重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。

一般来说,当函数的参数有多个类型时,我们可以定义一个联合类型:

type Combinable = string | number;

function fun (a: Combinable, b: Combinable) {
  if (typeof a === 'string' || typeof b === 'string') {
    return a.toString() + b.toString();
  }
  return a + b;
}

但是当我们需要对返回的结果进行进一步的处理的时候,就会出现问题:

const res = fun('a' + 'b');
res.split(''); // ts error:类型“number”上不存在属性“split”

因为返回的res值有可能是number类型的,因此想要解决这种问题,就得需要用到 函数重载;

type Combinable = string | number;

function fun (a: string, b: string): string;
function fun (a: number, b: number): number;
function fun (a: string, b: number): string;
function fun (a: Combinable, b: Combinable) {
  if (typeof a === 'string' || typeof b === 'string') {
    return a.toString() + b.toString();
  }
  return a + b;
}

const res = fun('a', 'b');
res.split('');

const res2 = fun(1, 2);
res2.split(''); // ts error: 类型“number”上不存在属性“split”。

上面代码可以见到,使用函数重载之后,ts可以根据对应的函数类型定义来给出特定的错误提示。

元组(Tuple)

元组是TypeScript中特有的类型,工作方式类似数组,最重要的特性是 可以限制数组元素的个数和类型,适合用来实现多值返回。

用法:

let arr: [string, number, boolean];
arr = ['hello world', 1, true];
arr = ['hello world', 1, 1]; // ts error: 不能将类型“number”分配给类型“boolean”。
arr = ['hello world', 1]; // ts error: 不能将类型“[string, number]”分配给类型“[string, number, boolean]”。 源具有 2 个元素,但目标需要 3 个。

元组类型只能表示一个已知元素数量和类型的数组,长度已指定,越界访问会提示错误。

元组的解构赋值

元组的解构赋值跟正常数组一样,但是要注意:解构数组元素的个数不能超过元组中元素的个数,不然会报错:

const arr: [string, number] = ['hello', 1]
const [a, b, c] = arr; // ts error: 长度为 "2" 的元组类型 "[string, number]" 在索引 "2" 处没有元素。ts(2493)

元组的可选元素

元组的可选元素也用 ?来表示(当然,必选元素也不能放在可选元素后面):

type ArrType = [string, number?, boolean?];

const arr1: ArrType = ['hello'];
const arr2: ArrType = ['hello', 1];

元组的剩余元素

元组的剩余元素用 ...X[]来表示,其中X为任意的类型,如 numberstring;

type ReastArrType = [number, ...string[]];

const arr: ReastArrType = [1, 'hello', 'hi'];

只读的元组类型

在元组类型前加上关键字 readonly,可以使其成为只读元组,一般通过const定义数组之后,可以通过数组的各种方法(push,pop)改变数组,因此加上 readonly关键字,既可以让任何企图修改元组中元素的操作都抛出异常:

const arr: readonly [number, string] = [1, 'hello'];

arr[1] = 'hi'; // ts error: 无法分配到 "1" ,因为它是只读属性。ts(2540)
arr.push(1); // ts error: 类型“readonly [number, string]”上不存在属性“push”。ts(2339)

枚举(Enum)

枚举是对js标准数据类型的补充,声明一组带名字的常量

用法如下:

enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};
console.log(Days['Sun']) // 0

被枚举的成员可以通过像对象一样的调用方式来访问,枚举成员就像对象中的Key,会默认被赋值为从0开始递增的数字。

当然,可以给枚举成员手动赋值:

// 当手动赋值的值都为number类型时,没有手动赋值的枚举成员的值会跟根据前一个成员的值递增
enum Days1 {Sun = 100, Mon = 200, Tue = 123, Wed, Fri, Sat};
console.log(Days1['Sun']) // 100
console.log(Days1['Wed']) // 124

// 这里Fir没有手动赋值,但它前面一项手动赋值为string,因此无法自动递增,所以会报错
enum Days2 {Sun = 100, Mon = 200, Tue = 123, Wed = '星期三', Thu = '星期四', Fri, Sat}; // ts error:枚举成员必须具有初始化表达式。

// 这里将Fir手动赋值为200,那么Sat会自动赋值为201,也不会报错
enum Days3 {Sun = 100, Mon = 200, Tue = 123, Wed = '星期三', Thu = '星期四', Fri = 200, Sat};

所以:枚举可以分为数字枚举、字符串枚举异构(混入)枚举

数字枚举

数字枚举的特点在于,它经过编译之后会生成反向映射表,也就是说数字枚举除了生成键值对的集合,还会生成值键对的集合:

enum Days1 {Sun = 100, Mon = 200, Tue = 123, Wed, Fri, Sat};
console.log(Days1['Sun']) // 100
console.log(Days1[100]) // 'Sun'

字符串枚举

顾名思义,字符串枚举就是通过手动赋值将枚举成员的值都改成string类型的枚举,但是字符串枚举就不会生成反向映射。

异构(混入)枚举

名字看着唬人一点,就是枚举成员的值既有string类型的值又有number类型的枚举。

上述枚举分类是按照枚举成员类型来分别的,但是根据枚举的声明方式,又可以分成四种:普通枚举const枚举外部枚举外部常量枚举

普通枚举

话不多说,普通枚举就是直接使用关键词 + 枚举名称声明的枚举:

enum Days { a, b, c };

const枚举

const枚举,就是使用const修饰符来强调当前枚举类型,用这种方式声明,

const enum Days { a, b, c }
console.log(Days[0]) // ts error: 只有使用字符串文本才能访问常数枚举成员。

const enum Days1 { a, b, c }
console.log(Days[0]) // a

外部枚举

外部枚举,就是使用 declare关键词来声明一个枚举,这种方式声明的枚举,既不会生成反向映射,也无法对枚举成员进行访问

declare enum Days { a, b, c };
console.log(Days['a']) // Days is not defined

**作用:**外部枚举用来描述已经存在的枚举类型的形状,也就是说外部枚举是为了描述当前环境中已经存在的对象,这个对象可以存在于任意的地方,但一定是已声明的,这样就能防止枚举命名冲突和成员冲突。

外部常量枚举

外部常量枚举,就是同时使用 declareconst关键词联合声明的枚举类型,这个枚举类型和const枚举类型并没有什么区别,只是会提示是否有枚举命名冲突和成员冲突,也不会生成反向映射。

外部枚举外部常量枚举,在这里只做个记录和了解0.0不做更加深入的探究。。。

void

void表示没有任何类型,和其他类型是平等关系,不能直接赋值给其他类型;

nullundefined可以赋值给void类型;

一般我们在函数没有返回值时去声明一个 void类型的变量,方法在没有返回值时返回 undefined,但是需要将其定义成 void,否则会报错(声明类型不为 "void" 或 "any" 的函数必须返回值);

never

never表示那些永远不存在的值的类型,举例几种值永不存在的情况:

  1. 函数执行时抛出异常
function fun (x: string): never {
  throw new Error('错了');
}
  1. 函数中执行无限循环的代码
function fun (): never {
  while (true);
}
  1. 联合类型中类型A和类型B不存在交集(从类型定义来推断的值永不存在)
type A = 'A';
type B = 'B';
type C = A & B; // type C = never; 因为C不可能既等于'A'还等于'B',因此这个值永不存在

nevernullundefined一样,也是任何类型的子类型,可以赋值给任何类型,但是除了 never本身之外,没有任何类型的值可以赋值给它(any类型也不行)。

never类型的作用:

type Foo = string | number;
function fun (foo: Foo) {
  if (typeof foo === 'string') {
  } else if (typeof foo ===  'number') {
  } else {
    let check: never = foo;
  }
}

// 因为通过类型判断之后将foo的string和number类型都处理了(类型收窄),因此在最后一个else中foo的类型就是never,因为按照目前的类型定义,foo不会再有其他新的类型值了
// 这样做的好处是,在后续将Foo类型变为string | number | boolean时,check就会报错

any

any允许被赋值为任意类型;

any上访问任何属性都是允许的,也允许调用任何方法;

如果在变量声明的时候,未指定其类型,那么它会被默认识别为 any

但是any太过宽松,尽量不要使用。

unknown

unknown是为了解决 any所带来的问题,它与 any相似,任何类型都可以分配给 unknown

any的分别:任何类型的值都可以赋值给 unknown,但是 unknown类型只能分配给 unknowanyany类型的值可以赋值给任何类型)。

let a: unknown = 'hello';
let b: any = 'hi';

b = a;
a = b;

let c: string = 'nice';
c = a; // ts error: 不能将类型“unknown”分配给类型“string”。
c = b;

let d: unknown;
d = a;

对于 unknown类型的值,需要通过 typeof类型断言等方式来缩小未知范围,这种机制比直接使用 any会显得更加的安全:

function getInfo () {
  return '123';
}

const user: unknown = { getInfo: getInfo };
user.getInfo(); // ts error: 类型“unknown”上不存在属性“getInfo”。ts(2339)

// 类型断言
(user as { getInfo: () => string }).getInfo();

// typeof
let x: unknown = '123';

x.split(''); // ts error: 类型“unknown”上不存在属性“split”。ts(2339)

if (typeof x === 'string') {
  x.split('');
}

Number、String、Boolean、Symbol

这些都属于 对象类型,与对应的原始类型 numberstringbooleansymbol不同;

原始类型兼容对象类型,但是对象类型不兼容对应的原始类型;

也就是说类型为 number的值可以赋值给类型 Number,但是类型 Number的值不能赋值给类型 number;

let num: number = 1;
let Num: Number = 2;

Num = num;
num = Num;
// te error: 不能将类型“Number”分配给类型“number”。
 //“number”是基元,但“Number”是包装器对象。如可能首选使用“number”。ts(2322)

根据ts提示,最好使用 原始类型而不是 对象类型来注释值的类型;

object、Object、{}

{}Object可以互相代替,用来表示原始类型(nullundefined除外)和非原始类型;而 object则单纯表示非原始类型;

原始类型:string、boolean、number、bigint、symbol、null、undefined

const obj: Object = 'dddd';
const obj1: object = 'dddd'; // ts error: 不能将类型“string”分配给类型“object”。

类型推断

TS可以基于赋值表达式,推断出对应变量的类型,这种能力被称为 类型推断

具体来说,就是TS中,具有初始化值的变量有默认值的函数参数函数返回的类型,都可以根据上下文推断出来。

当然,如果定义的时候没有赋值,那么不管之后变量的值是否存在,都会被自动推断成any而完全不被类型检查。

类型断言

类型断言简单点来说就是直接告诉TS这个变量是什么类型,让它放心大胆的执行。

类型断言有两种写法:

  1. 值 as 类型
  2. <类型>值

但是在react的JSX中,<类型>这种写法会产生语法冲突,因为它除了表示类型断言之外,也可能是表示一个 泛型

interface A {
  run: () => void;
}

interface B {
  go: () => void;
}

function fun (c: A | B) {
  c.run(); // ts error: 类型“A | B”上不存在属性“run”。
  	   // 类型“B”上不存在属性“run”。ts(2339)
}

// 使用断言
function fun (c: A | B) {
  (c as A).run(); // true
}

注意:类型断言某种程度上是在欺骗TS编译器,无法避免运行时的错误,因此不能滥用类型断言;就像上诉例子,虽然没有报错了,

但是如果c = { go: () => {}},就会出错,因为方法run不存在。

所以:类型断言并不是类型转换,它不会真的影响到变量的类型。

当然,并不是任何一个类型都可以被断言为任何另一个类型

只有在类型A兼容类型B的时候,类型A能够被断言为B,B也能被断言为A;(可以将任意类型断言为 any

interface A {
  run: () => void;
}

interface B {
  go: () => void;
}

function fun (c: A | B) {
  (c as string).toString();
  // ts error: 类型 "A | B" 到类型 "string" 的转换可能是错误的,因为两种类型不能充分重叠。如果这是有意的,请先将表达式转换为 "unknown"。
  // 类型“B”不可与类型“string”进行比较。ts(2352)
}

非空断言

非空断言通过在变量后面添加 !来断言操作值是非 nullundefined类型:

let a: string | null;
a.split(''); // ts error: 在赋值前使用了变量“a”。ts(2454) 对象可能为 "null"。ts(2531)
a!.split(''); // true;

直接在声明变量时使用 非空断言,可以告诉编译器该属性会被明确地赋值:

let a: string;
a.split(''); // ts error: 在赋值前使用了变量“a”。ts(2454)

// 使用非空断言
let a!: string;
a.split('');

双重断言

因为:

  • 任何类型都可以被断言为 any
  • any可以被断言为任何类型

那么:

我们可以使用双重断言 as any as 类型来将类型断言为任何另一个类型。

注意:这种方式断言的类型基本上都是错误的,很有可能导致运行时的错误

字面量类型

目前TS支持三种字面量类型:字符串、数字、布尔值

let a: 'hello' = 'hello';
let b: 1 = 1;
let c: true = true;

这种单一的类型其实单独使用没啥大用,主要作用在与 联合类型一起使用时,可以将变量类型限定为更具体的类型,提升了程序的可读性:

let sex = '男' | '女';

联合类型( | )

联合类型表示取值可以为多种类型中的一种,使用 |分隔每个类型;

let a = number | string;

类型别名( type )

使用 type给类型取了一个新的名字,而不是创建了一个新的类型

type NewType = string;

交叉类型( & )

交叉类型可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性,使用 &定义,但是单纯地把原始类型等合并成交叉类型,是没有任何用处的,在前面 never类型的说明中也有介绍,一个值不可能同时满足两种原始类型,因此这个变量的类型一定是 never

交叉类型真正的作用在于将多个接口类型合并成一个类型,从而实现所谓的合并接口类型:

interface A {
  name: string;
}

interface B {
  age: number;
}

type Person = A & B;

let jack: Person = {
  name: 'jack',
  age: 18,
}

注意点,当两个接口类型中存在同名属性时:

  • 如果该同名属性类型一个是 string,一个是 number,那么合并后就是一个交叉类型 string & number,就变成了 never类型;
  • 如果该同名属性类型一个是 number,一个是 number的子类型,那么合并后该属性的类型就是两者中的子类型;
  • 如果该同名属性时非基本数据类型,那么会将该属性下的数据类型进行合并:
interface A {
  jack: {
    name: string;
  }
}

interface B {
  jack: {
    sex: string;
  }
}

interface C {
  jack: {
    age: number;
  }
}

type Person = A & B & C;
let person: Person = {
  jack: {
    name: 'jack',
    sex: '男',
    age: 18,
  }
}

接口(interface)

在TS中,使用接口(interface)来定义对象的类型;也可以用它来对类的一部分行为进行抽象

定义对象的类型

  1. 基本用法:
interface Person {
  name: string;
  age: number;
  sex: '男' | '女' | '不好说'
}

const person: Person = {
  name: 'jack',
  age: 22,
  sex: '男'
}

赋值的时候,变量的形状必须和接口的形状保持一致。

  1. 可选 | 只读属性 | 任意属性
interface Person {
  readonly name: string; // 只读属性
  age?: number; // 可选属性
  [key: string]: any // 任意属性
}

var person: Person = {
  name: 'tao'
}

person.name = 'wyt'; // ts error: 无法分配到 "name" ,因为它是只读属性。ts(2540)

只读属性的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候。

interface Person {
  readonly name?: string; // 只读属性
}

var person: Person = {};

person.name = 'wyt'; // ts error: 无法分配到 "name" ,因为它是只读属性。ts(2540)

任意属性在定义了之后,那么确定属性和可选属性的类型都必须是任意属性的类型的子集

interface Person {
  name: string;
  age?: number; // ts error: 类型“number”的属性“age”不能赋给“string”索引类型“string”。ts(2411)
  [key: string]: string;
  // [key: string]: string | number
}

let person: Person = {
// te error: 不能将类型“{ name: string; age: number; gender: string; }”分配给类型“Person”。
// 属性“age”与索引签名不兼容。
// 不能将类型“number”分配给类型“string”。ts(2322)
  name: 'Tom',
  age: 25,
  gender: 'male'
};

鸭式辩型法

像鸭子一样走路并且嘎嘎叫的就是鸭子,也就是说当一个对象interface定义类型的属性,则被认为这个对象的类型跟interface定义的类型相同。

interface Person {
  name: string
};

function getInfo (person: Person) {
  return person.name;
}

// 这里的对象a多了一个sex属性,但是它还是有name属性,因此默认它也是一只"鸭子"。
var a = {
  name: 'jack',
  sex: '男',
}

getInfo(a);

TS中类的用法

  1. publicprivateprotected访问修饰符
  • public修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public
class Person {
  public name;
  public constructor(name) {
    this.name = name;
  }
}
let p = new Person('wyt');
console.log(p.name); // 'wyt'
p.name = 'www';
console.log(p.name) // 'www'
  • private修饰的属性或方法是私有的,不能在声明它的类的外部访问(TS编译后的代码中,没有限制 private属性在外部的可访问性)
class Person {
  private name;
  public constructor(name) {
    this.name = name;
  }
}
let p = new Person('wyt');
console.log(p.name); // ts error: 属性“name”为私有属性,只能在类“Person”中访问
  • protected修饰的属性或方法是受保护的,它和 private类似,区别是 protected在子类中是允许被访问的。而 private修饰的属性或方法在子类中也是不允许被访问的。
class Parent {
  protected name;
  constructor(name) {
    this.name = name;
  }
}

class Child extends Parent {
  constructor(name) {
    super(name);
    console.log(this.name);
  }
}
  1. readonly只读属性关键字,只允许出现在属性声明或者索引签名或构造函数中。
class Person {
  readonly name;
  constructor(name) {
    this.name = name;
  }
}
let p = new Person('wyt');
p.name = 'www'; // ts error: 无法分配到 "name" ,因为它是只读属性。ts(2540)

如果 readonly和其他访问修饰符同时存在的话,就需要将其写在其他访问修饰符的后面。

  1. 抽象类(abstract)

抽象类的概念:

  • 抽象类不允许被实例化
  • 抽象类中的抽象方法必须被子类实现
abstract class Parent {
  name;
  constructor(name) {
    this.name = name;
  }
  abstract getName();
}
let p = new Parent('wyt'); // ts error: 无法创建抽象类的实例。ts(2511)

class Child extends Parent {
  getName() {
    console.log('hello, my name is ' + this.name);
  }
}

let c = new Child('wyt');

c.getName();

类的类型

给类加上类型与接口类似:

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

类与接口

把不同类之间的一些共有的特性提取成接口(interfaces),用 implements关键字来实现。

不同的类之间可能会有一些共同的特性,这个时候就可以吧特性提取成接口(interface);用 implements来实现,可以提高面向对象的灵活性

interface Say {
  sayHi: () => void;
}

class Mother implements Say {
  sayHi = () => {
    console.log('hi');
  };
}

interface Eat {
  eatFood: () => void
}

// 当有多个接口的时候还可以这么写
class Father implements Say, Eat {
  sayHi = () => {
    console.log('hi');
  };
  eatFood = () => {
    console.log('eat')
  }
}

接口可以继承类

在TS创建类的时候,同时也会创建一个跟类名称相同的类型,也就是说,创建类Person的时候还会创建类型Person,所以类可以被接口继承

综上,类Person,也可以当做一个类型来使用,所以接口继承类和接口继承接口没有什么本质的区别。

这里可以提一点个人理解,在我们使用antd的Input组件时候,有时候需要用到Ref,这个时候我们会使用useRef来创建一个ref对象:

import { useRef, useEffect } from 'react';
import { Input } from 'antd';

const Test = () => {
  // 这个时候,就可以将Input当做类型
  const inputRef = useRef<Input>(null);

  useEffect(() => {
    console.log(inputRef.current?.input.value);
  }, []);

  return (
    <Input ref={inputRef} />
  )
}

泛型

泛型是指在定义函数、接口或者类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

简单来讲,泛型是通过参数化类型来实现在同一份代码上操作多种数据类型

泛型的一般使用

type Type<T> = T;
const name: Type<string> = 'wyt';

interface Type2<T> {
  name: T;
}
const name2: Type2<string> = {
  name: 'wyt'
}

泛型约束

因为在使用泛型变量的时候,实现会不知道他是哪种类型,因此就不能随意操作它的属性或方法,所以我们需要对类型参数作一定的限制,比如希望传入的参数都具有length属性:

function test<T>(info: T): number {
  return info.length; // ts error: 类型“T”上不存在属性“length”
}

// 正确使用
function test<T extends { length: number }>(info: T): number {
  return info.length;
}

泛型函数的调用

function test<T>(name: T): T {
  return name;
}
// 第一种调用方式,手动明确T的类型;
test<string>('wyt');
// 第二种调用方式,根据typescript自己的类型推断;
test('wyt');

箭头函数和普通函数中使用泛型

// 普通函数
function test<T>(name: T): T {
  return name;
}
// 箭头函数,注意逗号
const test2 = <T,>(name: T): T => {
  return name;
}

泛型参数的默认类型

// ts2.3之后,当使用泛型时没有在代码中直接指定类型参数或者ts本身无法推断类型时,就会使用到这个默认类型
function test<T = string>(name: T): T {
  return name;
}

泛型工具类型

泛型工具类型可以方便开发者使用TypeScript:

typeof

typeof的主要用途是在类型上下文中获取变量或者属性的类型:

interface Person {
  name: string;
  age: number;
}

const person: Person = {
  name: 'wyt',
  age: 100
}

type CopyType = typeof person;

const person2: CopyType = { // ts error: 类型 "{ name: string; }" 中缺少属性 "age",但类型 "Person" 中需要该属性。
  name: 'hahaha';
}

也可以用来获取函数的类型:

function test (bool: boolean): boolean {
  return bool;
}
type Func = typeof test; // (bool: boolean) => boolean
keyof

keyof可以用于获取某种类型的所有键,返回一个联合类型:

interface Person {
  name: string;
  age: number;
}

type KeyType = keyof Person; // 这里的类型为联合类型'name' | 'age';

type Person2 = string;

type KeyType2 = keyof Person2;
// type KeyType2 = number | typeof Symbol.iterator | "toString" | "charAt" | "charCodeAt" | "concat" | "indexOf" | "lastIndexOf" | "localeCompare" | "match" | "replace" | "search" | "slice" | ... 35 more ... | "matchAll"

这里需要注意的是,typeof后面跟的是值,而keyof后面跟的是类型

在接口存在任意属性的时候,使用 keyof就只返回 string | number类型;因为接口定义一个对象,而对象索引有可能是number也有可能为string:

interface Person {
  [key: string]: string;
}

type CopyType = keyof Person; // string | number

记录keyof的一种使用场景

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

fun({name: 'hahahha'}, 'name');
fun({name: 'hahahha'}, 'age'); // ts error: 类型“"age"”的参数不能赋给类型“"name"”的参数。

这样就能增加对象和它key值本身的一个联系。

in

in用来遍历联合类型:

type Keys = 'a' | 'b' | 'c'

type Obj = {
  [p in Keys]: string;
}
// 等于
// type Obj = {
//  a: string;
//  b: string;
//  c: string;
//}

in可以配合keyof一起使用:

interface Obj {
  name: string;
  age: number;
}

type Obj1 = {
  [p in keyof Obj]: string | number;
}

// 等于
// type Obj1 = {
//   name: string | number;
//   age: string | number;
// }
extends

在JS中,extends一般和class一起使用,用于继承父类的方法和属性;

在TS中,extendes除了像JS中的继承之外,还可以用于:

  • 继承/扩展类型
  • 泛型约束
  • 条件类型
  • 高阶类型

继承/扩展类型

interface Animal {
  name: string;
}

interface Dog extends Animal {}

const dog: Dog = {
  name: 'wenming'
}

泛型约束

见上文,其实我更乐意把它理解成让泛型T继承某个类型。

条件类型

extends可以用在类型的三元表达式里,用来判断左边的类型是否可以分配给右边的类型:

type Example = string extends number ? string : number;
const value: Example = 12;

type Example1 = string extends any ? string : number;
const value1: Example1 = 12; // ts error: 不能将类型“number”分配给类型“string”。

高阶类型

extends实现动态判断传入的类型是否满足某一条件:

type Extracts<T, U> = T extends U ? T : never;
type KeysType = Extracts<string, number>; // never
type KeysType2 = Extracts<'a' | 'b', 'a' | 'b' | 'c'>; // 'a' | 'b'
infer

infer的用来配合extends使用,作用是在extends条件类型中声明一个类型,这个类型是用来占位的,代替后续被infer推导出来的类型,然后放在extends为true的位置里返回:

type GetType<T> = T extends infer R ? R : never;
type Test = GetType<string>; // Test为string;

根据上面的例子可以看出,infer的作用是声明一个类型R,这个R的具体值由 infer自己推断,用来满足 T extends infer R成立,即使其为true,R就是那个能够满足条件的类型。

infer的一些使用例子

  1. 元组转联合类型:将[string, number] 转为string | number;
type ChangeType<T> = T extends (infer R)[] ? R : never;
type Test = ChangeType<[number, string]>;

当然,这个转换直接用 keyof [number, string]就可以了。

  1. 推断出未知类型

这个是我觉得 infer比较有用的地方,下面来看使用场景:封装一个useEffect。

如果让我们自己手动封装一个具备防抖功能的useEffect,叫useSebounceEffect,那么就需要知道useEffect所需要的两个参数类型是啥,这时就轮到 infer出场了:

// 这里将infer U单体函数类型的第一个参数,然后传入useEffect的类型,然后让infer自动推断出第一个参数U的具体类型并返回,后面的ReturnSecondType也是同理。
type ReturnFirstType<T> = T extends (first: infer U) => unknown ? U : never;

type ReturnSecondType<T> = T extends (first: unknown, second: infer U) => unknown ? U : never;

const useDebounceEffect = (
  // 通过 `typeof useEffect`来获取useEffect的类型,
  fn: ReturnFirstType<typeof useEffect>
  dependencies?: ReturnSecondType<typeof useEffect>
) => {
  // 具体实现这里不论
  // const constFn = useCallback(debounce(fn), []);
  // useEffect(constFn, dependencies);
};

当然,这只是一个例子,像我们一般的使用中,useEffect的第一个参数类型可以直接通过React.EffectCallback来获取,但是如果对于类型没有直接导出的变量,那么通过 infer来获取是最合适不过的了。

看个小题目加深理解

interface Action<T> {
  payload?: T; type: string;
}

// 假设有Modle这样一个interface
interface Module {
  count: number;
  message: string;
  asyncMethod<T, U>(action: Promise<T>): Promise<Action<U>>;
  syncMethod<T, U>(action: Action<T>): Action<U>;
}

// 实现type Connect
// 保留属性为函数类型,其余的摒弃掉
// 把函数类型转化为<T, U>(args: T) => Action<U>
type Connect<T> = /** 你需要实现的逻辑 */

// type Result = Connect<Module>;
// Result = {
//   asyncMethod<T, U>(input: T): Action<U>;
//   syncMethod<T, U>(action: T): Action<U>;
// }

这个题目其实就是需要我们先挑选出类型中的函数类型的key,然后再使用条件类型+infer获取key对应的类型;

首先我们来获取类型Module中是函数类型的key:

interface Module {
  count: number;
  message: string;
  asyncMethod<T, U>(action: Promise<T>): Promise<Action<U>>;
  syncMethod<T, U>(action: Action<T>): Action<U>;
}

// 获取Module中是函数类型的key,返回一个联合类型
/**
 * 在这里解释一下这个PickFuncProp的一些利用到的知识点:
 * 1. 通过keyof Module获取到所有Module类型中的key,是一个联合类型;
 * 2. 通过in来枚举联合类型,使其称为新类型的key;
 * 3. 通过T[P](其实也就是Module<P>,P可能为'count','message','asyncMethod','syncMethod')来获取对应的类型;
 * 4. 再通过extends来判断T[P]是否为函数类型,即Function,如果是函数类型,那么就返回类型P,如果不是,返回never;
 * 5. 因此[P in keyof Module]: Module[P] extends Function ? P : never;返回的类型应该是这样的:
 * {
 *   count: never;
 *   message: never;
 *   asyncMethod: 'asyncMethod';
 *   syncMethod: 'syncMethod';
 * }
 * 6. 这里需要补充两点,因为之前自己不了解,所以加个解释:
 *    (1). 一般我取对象中某个属性对应的类型都是'类型[属性名]'的方式,如取Module中的count属性的类型就是Module['count'],
 *    但我在这里学习到,可以在后面的[]中写入对象属性的联合类型(当然这个联合类型所包含的类型需要对象中存在)来获取属性的对应类型,返回的是一个类型的联合类型;
 *    比如我要同时从Module中取出message和count的类型,那么就是Module['count' | 'message'];返回的类型为number | string;
 *    (2). 如果定义的联合类型中包含never类型,那么这个联合类型将省略never,也就是说:
 *    type Person = {
 *      name: 'name';
 *      age: number;
 *      height: never;
 *    }
 *    type Test = Person['name' | 'age' | 'height']; // 这里的Test就是number | "name" | never 就变成了number | "name"
 *  7. 因此,PickFuncProp<Module>中最后的结果就是:
 *     {
 *       count: never;
 *       message: never;
 *       asyncMethod: 'asyncMethod';
 *       syncMethod: 'syncMethod';
 *     }['count' | 'message' | 'asyncMethod' | 'syncMethod'];
 *     根据第六点,结果就为'asyncMethod' | 'syncMethod';
 */
type PickFuncProp<T> = {
  [P in keyof T]: T[P] extends Function ? P : never;
}[keyof T];

type FunKey = PickFuncProp<Module>; // "asyncMethod" | "syncMethod";

在获取完Module类型中对应的函数类型的key之后,就需要使用infer来实现函数的转换:

type TransitionFunc<F> = F extends (action: Promise<infer T>) => Promise<Action<infer U>> 
  ? <T, U>(action: T) => Action<U>
  : F extends (action: Action<infer T>) => Action<infer U>
  ? <T,U>(action: T) => Action<U>
  : F;
// 这里通过infer T和infer U去替代原来类型的位置,为的就是把Connect中asyncMethod和syncMethod的类型中的泛型与Module中的类型同步;

最后就是组合一下:

type Connect<T> = {
  [P in PickFuncProp<T>]: TransitionFunc<T[P]>;
};

type Result = Connect<Module>;
// type Result = {
//   asyncMethod: <T, U>(action: T) => Action<U>;
//   syncMethod: <T, U>(action: T) => Action<U>;
// }
索引类型

索引类型其实就是对一般对象更进一步的约束:

const person = {
  name: 'wyt',
  age: 100
}

type Person = typeof person;

function getInfo (person: any, key: string) {
  return person[key];
}

getInfo(person, 'name');
getInfo(person, 'height'); // 这里返回的是undefined

// 从上述例子可以看出,当key为'height'时,ts不会报错,但是返回的是undefined,因此要改动一下getInfo的类型
function getInfoN <T>(person: T, key: keyof typeof person) {
  return person[key]; 
}

getInfoN(person, 'name');
getInfoN(person, 'height'); // ts error: 类型“"height"”的参数不能赋给类型“"name" | "age"”的参数。

简而言之,就是专门对参数key的类型做出的限制罢了,本来key为string,改过之后的key是一个属性名的联合类型,这样ts就能检测出来传入的key是不是符合要求的。

映射类型

映射类型,即根据旧的类型创建出新的类型;

具体点来说就是复制一个类型,然后可以把类型中所有属性的可选和只读修饰符通过'+'或'-'进行修改;从而生成一个新的类型:

interface Person {
  readonly name: string;
  readonly age: number;
  readonly sex: '男' | '女';
}

// 根据Person类型生成一个映射类型,并且将name属性改成只读,把age属性的只读移除,把sex属性的可选移除
type CopyPerson = {
  -readonly [key in keyof Person]+?: Person[key];
}

// type CopyPerson = {
//   name?: string | undefined;
//   age?: number | undefined;
//   sex?: "男" | "女" | undefined;
// }
Partial

Partial就是将类型的属性变成可选,算是属于映射类型的工具类,使用方式如下:

interface Person {
  name: string;
  age: number;
}

let person: Partial<Person> = {
  name: 'wyt';
}
// Partial<Person> 的类型为{ name?: string; age?: number; }

其实 Partial就是一个TS内部帮我们封装好了的映射:

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

Partial存在一个局限性,就是只支持第一层的属性,如果属性的类型中还包括接口的,那么就无法进行处理:

interface Person {
  name: string;
  age: number;
  info: {
    height: number;
    weight: number;
  }
}

const person: Partial<Person> = {
  name: 'wyt',
  info: {        // ts error: 类型 "{ height: number; }" 中缺少属性 "weight",但类型 "{ height: number; weight: number; }" 中需要该属性。
    height: 180,
  }
}

所以可以封装一个能够处理多层的DeepPartial:

type DeepPartial<T> = {
  [key in keyof T]?: T[key] extends object ? DeepPartial<T[key]> : T[key];
}
Required

Required的用法与 Partial相反,就是将属性都变成必选,使用方式不多说,定义就是将 Partial的'+'改为'-':

type Required<T> = {
  [key in keyof T]-?: T[key];
}
Readonly

Readonly就是将类型的属性变成只读,用法与上面的可选跟必选一样,定义如下:

type Readonly<T> = {
  readonly [key in keyof T]: T[key]; // readonly前面的'+'号可以省略;
}
Pick

Pick的功能是从某个类型中挑出一些属性出来:Pick<T, Union>

interface Person {
  name: string;
  age: number;
  height: number;
}

type Height = Pick<Person, 'height'>;
type HeightName = Pick<Person, 'height' | 'name'>;

Pick的实现:

// 传入需要挑选类型的属性名的联合类型,通过keyof将其变成接口类型,再通过in遍历生成新的类型;
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
}
Record

Record的作用是将一个联合类型中的类型转换成另一个类型的属性,并将这个属性的类型变成另一个类型:

type Person = 'name' | 'age';

type Com = number;

type Test = Record<Person, Com>;
// {
//   name: number;
//   age: number;
// }
// 也就是说将联合类型Person中的'name'和'age'变成类型为number的属性;

Record的实现:

// extends keyof any用来约束输入的类型
type Record<K extends keyof any, T> = {
  [P in K]: T;
};
ReturnType

ReturnType的作用是得到一个函数的返回值类型:

type Test = ReturnType<() => number>; // number

ReturnType的实现:

// 体现了infer的强大了。。。
type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : void;
Exclude

Exclude的作用是将某个类型中属于另一个的类型移除掉:

type Test = Exclude<'a' | 'b' | 'c', 'c'>; // 'a' | 'b'

Exclude的实现:

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

Extract的作用与 Exclude相反,它是从一个类型中提取属于另一个类型的类型:

type Test = Extract<'a' | 'b' | 'c', 'c'>; // 'c'

Extract的实现:

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

Omit的作用是使用T类型中除了K类型的所有属性,来得到一个新的类型。

interface Person {
  name: string;
  age: number;
  height: number;
}

type Test = Omit<Person, "height">;
// type Test = {
//   name: string;
//   age: number;
// }

Omit的实现:

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
NonNullable

NonNullable的作用是用来过滤类型中的nullundefined类型:

type Test = string | null | undefined;
type Test1 = NonNullable<Test>; // string;

NonNullable的实现:

type NonNullable<T> = T extends null | undefined ? never : T;

通过 NonNullable的实现可以发现:NonNullable对对象中的属性类型为null和undefined是无效的,那么可以自行实现一个过滤对象类型的工具:

type GetType<T> = {
  [key in keyof T]: T[key] extends null | undefined ? never : key;
}

type InterNonNullable<K> = {
  [key in GetType<K>[keyof GetType<K>]]: GetType<K>[key] extends null | undefined ? never : K[key];
}

type Test1 = InterNonNullable<{age: null; name: undefined; height: number }>; // { height: number; }

当然,这只是对第一层的过滤,多层的过滤暂不多说了,原理都差不多....

Parameters

Parameters的作用是用来获得函数的参数类型组成的元组类型:其实就是将上面讲到的 用 infer获取useEffect参数类型的方式封装成拿来就能用了;

重新实现一下封装useSebounceEffect的方式:

const useDebounceEffect = (
  fn: Parameters<typeof useEffect>[0],
  dependencies?: Parameters<typeof useEffect>[1]
) => {
};

是不是变得更加方便快捷了~~

Parameters的实现:

// 这里的第一个extend用作类型约束,如果传入的不是函数类型而是string类型的话会报错:ts error:类型“string”不满足约束“(...args: any) => any”。
type Parameters<T extends (...arg: any) => any> = T extends (...args: infer K) => any ? K : never;

TS类型谓词——is关键字

is关键字一般用于函数返回值类型中,判断参数是否属于某一类型,并根据结果返回对应的布尔类型:

// 因为str的类型不确定,这时如果不使用is,直接将返回类型改成boolean,那么后续的foo.toFixed()将不会在编译时报错而是在运行时报错
function isString (str: any): str is string {
  return typeof str === "string";
}

function test (foo: any) {
  if(isString(foo)) {
    console.log("it is a string" + foo);
    console.log(foo.toFixed(2)); // ts error: 属性“toFixed”在类型“string”上不存在。
  }
}
test("hello world");

在上述例子中,TS进一步缩小变量的类型,将类型从any缩小至了string,并且类型保护的作用域仅仅在if后的块级作用域中生效。

接口(interface)与类型别名(type)的区别

  1. interface和type都可以用来定义对象或者函数,但是type可以声明基本类型别名,联合类型,元组等类型。
  2. type定义对象时,类型别名不能重复,而interface可以重复对某个接口定义属性和方法:
interface Person {
  name: string;
}

interface Person {
  sex: '男' | '女';
}

// 等价于

interface Person {
  name: string;
  sex: '男' | '女';
}

type Person2 = { // te error: 标识符“Person2”重复。ts(2300)
  name: string;
}

type Person2 = { // te error: 标识符“Person2”重复。ts(2300)
  sex: '男' | '女';
}
  1. 两者扩展方式不同,接口的扩展就是继承,通过 extends实现,而类型别名的扩展就是交叉类型,通过 &实现:
interface Person {
  name: string;
}

interface Person1 extends Person {
  sex: '男' | '女'
}

type Person2 = {
  age: number;
}

type Person3 = Person & {
  height: number;
}

interface Person4 extends Person2 {
  weight: number;
}
  1. type能使用 in关键字生成映射类型,但是interface不行

总的来说,一般能用interface 实现的,那就用interface实现,如果不能,那么再用type实现。

涉及参考资料