当我们谈论TypeScript时,我们在谈论什么?

134 阅读15分钟

TypeScript 与 JavaScript 的区别

TypeScriptJavaScript
JavaScript 的超集用于解决大型项目的代码复杂性一种脚本语言,用于创建动态网页
可以在编译期间发现并纠正错误作为一种解释型语言,只能在运行时发现错误
强类型,支持静态和动态类型弱类型,没有静态类型选项
最终被编译成 JavaScript 代码,使浏览器可以理解可以直接在浏览器中使用
支持模块、泛型和接口不支持模块,泛型或接口
社区的支持仍在增长,而且还不是很大大量的社区支持以及大量文档和解决问题的支持

它是JavaScript的一个超集,添加了可选的静态类型和面向对象编程

类型检查可以在运行时做,也可以运行之前的编译期做。这是两种不同的类型,前者叫做动态类型检查,后者叫做静态类型检查。

两种类型检查各有优缺点。动态类型检查 在源码中不保留类型信息,对某个变量赋什么值、做什么操作都是允许的,写代码很灵活。但这也埋下了类型不安全的隐患,比如对 string 做了乘除,对 Date 对象调用了 exec 方法,这些都是运行时才能检查出来的错误。

其中,最常见的错误应该是 “null is not an object”、“undefined is not a function” 之类的了,写代码时没发现类型不匹配,到了运行的时候才发现,就会有很多这种报错。

所以,动态类型虽然代码写起来简单,但代码中很容易藏着一些类型不匹配的隐患。

image.png

静态类型检查则是在源码中保留类型信息,声明变量要指定类型,对变量做的操作要和类型匹配,会有专门的编译器在编译期间做检查。

静态类型给写代码增加了一些难度,因为你除了要考虑代码要表达的逻辑之外,还要考虑类型逻辑:变量是什么类型的、是不是匹配、要不要做类型转换等。

不过,静态类型也消除了类型不安全的隐患,因为在编译期间就做了类型检查,就不会出现对 string 做了乘除,调用了 Date 的 exec 方法这类问题。

所以,静态类型虽然代码写起来要考虑的问题多一些,会复杂一些,但是却消除了代码中潜藏类型不安全问题的可能。

image.png

动态类型只适合简单的场景,对于大项目却不太合适,因为代码中可能藏着的隐患太多了,万一线上报一个类型不匹配的错误,那可能就是大问题。

而静态类型虽然会增加写代码的成本,但是却能更好的保证代码的健壮性,减少 Bug 率。

所以,大型项目注定会用静态类型语言开发

TypeScript 变量声明

变量是一种使用方便的占位符,用于引用计算机内存地址。

我们可以把变量看做存储数据的容器。

TypeScript 变量的命名规则:

  • 变量名称可以包含数字和字母。

  • 除了下划线 _ 和美元 $ 符号外,不能包含其他特殊字符,包括空格。

  • 变量名不能以数字开头。

cannot be compiled under ‘--isolatedModules‘ because it is considered a global script file. Add an i

找到tsconfig.json的配置文件:

isolatedModules字段改为false

原因:

Typescript将没有导入/导出的文件视为旧脚本文件。这样的文件不是模块,它们的任何定义都已合并到全局名称空间中。 isolatedModules禁止此类文件。

将任何导入或导出添加到文件都使其成为一个模块,并且错误消失。

export {}也是一种方便的方法,可以在不导入任何内容的情况下使文件成为模块(定义变量的时候不会有冲突)。不会有如下的问题:

注意:变量不要使用 name 否则会与 DOM 中的全局 window 对象下的 name 属性出现了重名。

any 类型

任意值是 TypeScript 针对编程时类型不明确的变量使用的一种数据类型,它常用于以下三种情况。

1、变量的值会动态改变时,比如来自用户的输入,任意值类型可以让这些变量跳过编译阶段的类型检查,示例代码如下:

let x: any = 1; // 数字类型 x = 'I am who I am'; // 字符串类型 x = false; // 布尔类型

改写现有代码时,任意值允许在编译时可选择地包含或移除类型检查,示例代码如下:

let x: any = 4; x.king(); // 正确,king方法在运行时可能存在,但这里并不会检查 x.toFixed(); // 正确

定义存储各种类型数据的数组时,示例代码如下:

let arrayList: any[] = [1, false, 'fine']; arrayList[1] = 100;

Null 和 Undefined

null

在 JavaScript 中 null 表示 "什么都没有"。

null是一个只有一个值的特殊类型。表示一个空对象引用。

用 typeof 检测 null 返回是 object。

undefined

在 JavaScript 中, undefined 是一个没有设置值的变量。

typeof 一个没有值的变量会返回 undefined。

Null 和 Undefined 是其他任何类型(包括 void)的子类型,可以赋值给其它类型,如数字类型,此时,赋值后的类型会变成 null 或 undefined。而在TypeScript中启用严格的空校验(--strictNullChecks)特性,就可以使得null 和 undefined 只能被赋值给 void 或本身对应的类型,示例代码如下:

// 启用 --strictNullChecks let x: number; x = 1; // 运行正确 x = undefined; // 运行错误 x = null; // 运行错误

上面的例子中变量 x 只能是数字类型。如果一个类型可能出现 null 或 undefined, 可以用 | 来支持多种类型,示例代码如下:

// 启用 --strictNullChecks let x: number | null | undefined; x = 1; // 运行正确 x = undefined; // 运行正确 x = null; // 运行正确

never 类型

never 是其它类型(包括 null 和 undefined)的子类型,代表从不会出现的值。这意味着声明为 never 类型的变量只能被 never 类型所赋值,在函数中它通常表现为抛出异常或无法执行到终止点(例如无限循环),示例代码如下:

let x: never; let y: number; // 运行错误,数字类型不能转为 never 类型 x = 123; // 运行正确,never 类型可以赋值给 never类型 x = (()=>{ throw new Error('exception')})(); // 运行正确,never 类型可以赋值给 数字类型 y = (()=>{ throw new Error('exception')})(); // 返回值为 never 的函数可以是抛出异常的情况 function error(message: string): never { throw new Error(message); } // 返回值为 never 的函数可以是无法被执行到的终止点的情况 function loop(): never { while (true) {} }

断言

1+true == 2 在ts环境 报错,在js可以

使用断言可以使等式成立 类型断言并不意味着,可以把某个值断言为任意类型。

const n = 1;
const m:string = n as string; // 报错

上面示例中,变量n是数值,无法把它断言成字符串,TypeScript 会报错。

类型断言的使用前提是,值的实际类型与断言的类型必须满足一个条件。

expr as T

上面代码中,expr是实际的值,T是类型断言,它们必须满足下面的条件:exprT的子类型,或者Texpr的子类型。

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

但是,如果真的要断言成一个完全无关的类型,也是可以做到的。那就是连续进行两次类型断言,先断言成 unknown 类型或 any 类型,然后再断言为目标类型。因为any类型和unknown类型是所有其他类型的父类型,所以可以作为两种完全无关的类型的中介。

// 或者写成 <T><unknown>expr
expr as unknown as T

上面代码中,expr连续进行了两次类型断言,第一次断言为unknown类型,第二次断言为T类型。这样的话,expr就可以断言成任意类型T,而不报错。

下面是本小节开头那个例子的改写。

const n = 1;
const m:string = n as unknown as string; // 正确

上面示例中,通过两次类型断言,变量n的类型就从数值,变成了完全无关的字符串,从而赋值时不会报错。

TypeScript中的 ?: 是什么意思

可选参数和可选属性

使用了 –strictNullChecks,可选参数会被自动地加上 | undefined:

function f(x: number, y?: number) {
    return x + (y || 0);
}
f(1, 2);
f(1);
f(1, undefined);
f(1, null); // error, 'null' is not assignable to 'number | undefined'


class C {
    a: number;
    b?: number;
}

let c = new C();
c.a = 12;
c.a = undefined; // error, 'undefined' is not assignable to 'number'
c.b = 13;
c.b = undefined; // ok
c.b = null; // error, 'null' is not assignable to 'number | undefined'

使用泛型(限制为 extends object)与直接使用 object 类型的区别?

使用具体类型的 printName 函数

	interface Named {  
	    name: string;  
	}  

	// 使用具体类型的函数  
	function printNameSpecific(obj: Named) {  
	    console.log(obj.name); // 直接访问 obj.name,因为 obj 是 Named 类型  
	}  

	// 使用示例  
	const person: Named = { name: 'Alice' };  
	printNameSpecific(person); // 输出: Alice    
	// 如果尝试传递一个非 Named 类型的对象,TypeScript 会报错  
	// const notNamed = { title: 'Mr.' };  
	// printNameSpecific(notNamed); // 类型错误

在这个例子中,printNameSpecific 函数只能接受 Named 类型的对象,并且只能访问该类型定义的属性。如果尝试传递一个不符合 Named 接口的对象,TypeScript 编译器会报错,从而保证了类型安全。

使用泛型的 printName 函数

	interface Named {  
	    name: string;  
	}  

	interface Employee extends Named {  
	    id: number;  
	    department: string;  
	}  

	// 使用泛型的函数  
	function printNameGeneric<T extends Named>(obj: T) {  
	    console.log(obj.name); // 直接访问 obj.name,因为 T 被约束为 Named  
	    // 可以在函数内部进行额外的类型检查或利用 T 的其他属性(如果适用)  
	    if ('id' in obj && 'department' in obj) {  
	        console.log(`Employee ID: ${obj.id}, Department: ${obj.department}`);  
	    }  
	} 
	// 使用示例  
	const person: Named = { name: 'Alice' };  
	printNameGeneric(person); // 输出: Alice  
	const employee: Employee = { name: 'Bob', id: 123, department: 'Engineering' };  
	printNameGeneric(employee); // 输出: Bob 和 Employee ID/Department 信息  
	// 也可以传递其他符合 Named 接口的对象  
	const customer: { name: string; email: string } = { name: 'Charlie', email: 'charlie@example.com' };  
	printNameGeneric(customer); // 输出: Charlie,但不会输出 email

在这个泛型版本的例子中,printNameGeneric 函数更加灵活,因为它可以接受任何符合 Named 接口的对象。这意呀着,只要对象具有 name 属性,无论它是否还有其他属性,都可以传递给这个函数。

此外,由于泛型的使用,我们可以在函数内部利用 T 的类型信息(尽管在这个例子中我们主要使用了 Named 接口的属性),并且可以在需要时添加额外的类型检查来访问特定类型的属性(如 Employee 的 id 和 department)。

ps: 泛型的相关知识

image.png

image.png

ps: image.png

高级类型介绍

在实际开发中,大家可能比较常用一些基础类型,比如string、number、boolean等,但是当我们了解了一些高级类型后,我们可以定义更大复杂,更加灵活的接口类型。

1. 联合类型(|)

联合类型的规则和逻辑\color{DarkTurquoise}{“或”}是一致的,表示类型是连接多个类型中的任意一个

T | U

// demo
interface IPerson {
  age: number;
  gender: '女' | '男';
}

const person: Iperson = {
  age: 25;
  gender: '女'
}

2. 交叉类型(&)

交叉类型可以将多个类型合并成一个类型,写法和逻辑\color{DarkTurquoise}{“与”}相同

 T & U
 
 // demo 现在有两个类,我们可以通过交叉类型来实现一个新的属性的类型定义
 interface IPerson {
   age: number;
   gender: string;
 }

 interface IJob {
   title: string;
   years: number;
 }

 const life: IPerson & IJob = {
    age: 30,
    gender: 'nv',
    title: '开发',
    years: 10,
  };

3. 类型别名(type)

类型别名,它允许你为类型创建一个名字,这个名字就是类型的别名,从而你可以在多处使用这个别名,并且有必要的时候,你可以更改别名的值(类型),以达到一次替换,多处应用的效果,类型别名与声明变量的语法很类似,只需要把const,let换成type关键字即可。

type Alias = T | U
 
 // demo
 type sex =  '女' | '男';
 
 interface IPerson {
  age: number;
  gender: sex;
}

4. 类型索引(keyOf)

keyof 类似于 Object.keys ,用于获取一个接口中 Key 的联合类型

// demo
interface IPerson {
  age: number;
  gender: sex;
}

// 使用了keyOf后,只要IPerson修改了,type类型也会跟着自动修改

type personKeys = keyof IPerson;
//等价于
type personKeys = 'age' | 'gender';

5. 类型约束(extends

extends主要是用来对泛型加以约束的,他不像class使用extends是为了达到继承的目的。

 // demo
 type BaseType = string | number | boolean;
 
 public testGenerics<T extends BaseType>(arg: T): T {
   return arg;
 }
 
 this.testGenerics('123'); // 成功
 this.testGenerics({}); // 失败

extends的应用场景
extends 经常与 keyof 一起使用,例如我们有一个方法专门用来获取对象的值,但是这个对象并不确定,我们就可以使用 extends 和 keyof 进行约束,具体例子如下:

// 根据传入的obj来约束key的值
function getValue<T, K extends keyof T>(obj: T, key: K) {
  return obj[key]
}

ps:在 typeScript 在不同的上下文中,extends 有以下几个语义。不同语义即有不同的用途:

  • 用于表达类型组合;
  • 用于表达面向对象中「类」的继承
  • 用于表达泛型的类型约束;
  • 在条件类型(conditional type)中,充当类型表达式,用于求值。

详情见:www.jb51.net/article/279…

6. 条件类型(U?X:Y)

条件类型的语法规则和三元表达式一致,一般用于一些类型不确定的情况

  T extends U ? X : Y // 如果T是U的子级 ,那么他的类型就是X,否则就是Y
  
  type Extract<T, U> = T extends U ? T : never; // 如果T是U的子级,那么返回T,否则抛弃

我们来看一下内置属性\color{DarkTurquoise}{Extract},他是用来提取公共属性的,接下来看一下实例:

 interface ITeacher {
  age: number;
  gender: sex;

}

interface IStudent {
  age: number;
  gender: sex;
  homeWork: string;
}

type CommonKeys = Extract<keyof ITeacher, keyof IStudent>; // "age" | "gender"

7. 类型映射(in)

in用来做类型映射,遍历已有接口的key或者遍历联合类型

type Test<T> = {
   [P in keyof T]: T[P];  
};
// keyof T  相当于 type ObjKeys = 'a' | 'b'
// P in ObjKeys 相当于执行了一次 forEach 的逻辑,遍历 'a' | 'b'

interface IObj {
  a: string;
  b: string;
}

type newObj = Test<IObj>;

type和interface的区别

类可以实现interface 以及 type(除联合类型外)

类无法实现联合类型

type Person = { name: string; } | { setName(name:string): void };
 
// 无法对联合类型Person进行实现
// error: A class can only implement an object type or intersection of object types with statically known members.
class Student implements Person {
    name= "张三";
    setName(name:string):void{// todo}
} 

索引签名问题

如果你经常使用TypeScript, 一定遇到过相似的错误:

Type ‘xxx’ is not assignable to type ‘yyy’
 
Index signature is missing in type ‘xxx’.

看个例子来理解问题:

interface propType{[key: string] : string
}
 
let props: propType
 
type dataType = {title: string
}
interface dataType1 {title: string
}
const data: dataType = {title: "订单页面"}
const data1: dataType1 = {title: "订单页面"}
props = data
// Error:类型“dataType1”不可分配给类型“propType”; 类型“dataType1”中缺少索引签名 
props = data1 

原因:interface定义的类型是不确定的, 后面再来一个:

interface propType{
    title:number
} 

这样propType类型就被改变了。

结论:

接口与类型别名的区别 type可以表示非对象类型,而interface只能表示对象类型;

interface可以继承type、class、interface,而type不支持继承;

同名interface会自动合并,同名type则会报错;

interface中可以使用this关键字,type不行;

type可以扩展原始数据类型,interface不行;

interface无法表达某些复杂类型(联合类型和交叉类型),但是type可以

官方推荐用 interface,其他无法满足需求的情况下用 type。

泛型及高级用法

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

泛型的本质是为了参数化类型、即不创建新类型,通过泛型就可以控制 函数/类/接口 接收形参时的具体类型。所以总结就是支持动态类型、以及可以实现类型约束

参数化类型

可以考虑以下2 个需求如何实现:

  1. 我们需要打印或获取某变量参数类型;
  2. 我们已有一个接口定义,现在只是期望把所有属性变成可选;

需要怎么实现呢?需求 1 暴力做法是把能想到的参数类型都枚举列出、显然麻烦一些、且不够灵活,因为我们并不期望预置固定类型、反而希望它能灵活处理、在调用时做限制:

// 暴力写法
function print(arg: string | number | boolean): string | number | boolean {
    console.log(typeof arg);
    return arg;
}

// 泛型解法
function print<T>(arg: T): T {
    console.log(typeof arg);
    return arg;
}

需求 2 暴力写法就是重新声明一份接口、加上可选修饰符?

// 暴力写法
interface IUser {
  name: string;
  age: number;
  sex: string;
}
interface IUserPartial {
  name?: string;
  age?: number;
  sex?: string;
}
const user: IUser = { name: 'cat' }; // Error: Type '{ name: string; }' is missing the following properties from type 'IUser': age, sexts(2739)
const user: IUserPartial = { name: 'cat' }; // ok

// 泛型
type Partial<T> = {
    [P in keyof T]?: T[P];
};
const user: Partial<IUser> = { name: 'cat' }; // ok

再比如实现数组/元组项交换例子:

function swap<T, U>(tuple: [T, U]): [U, T] {
    return [tuple[1], tuple[0]];
}

swap(['cat', 'dog']); // ['dog', 'cat']
swap([3, 'dog']); // ['dog', 3]

类型约束

另外、可以利用泛型约束更精准控制函数调用参数,更可预期、有助于提高代码质量和可维护性,比如我们实现对象merge 功能:

function merge<T extends U, U>(target: T, source: U): T {
    for (let id in source) {
        target[id] = (<T>source)[id];
    }
    return target;
}

const obj = { a: 1, b: 2, c: 3, d: 4 };

merge(obj, { b: 10, d: 20 }); // { a: 1, b: 10, c: 3, d: 20 }
merge(obj, { b: 10, e: 20 }); // Error: Property 'e' is missing in type '{ a: number; b: number; c: number; d: number; }' but required in type '{ b: number; e: number; }'.ts(2345)

另外关于类型约束常见的场景就是对接 API 实现数据请求功能,我们通常期望对 API 格式、字段值、请求库返回结构等做好约定、提高程序稳定性、以及开发效率:

// src/api/type.ts
// API 定义接口,可以是具体的 url,可选、可用于做请求 url拦截
interface IApi {
  '/app/list': any;
	'/app/detail': any;
	[key: string]: Record<string, unknown>;
}

// API 返回格式 接口
interface IResponseData<T> {
  code: 0 | 1 | 2 | 3;
  data: T;
  message: string;
}

// 具体数据格式接口
interface IUserData {
  name: string;
  age: number;
}

// 通用请求 类型定义
type TCommonRequest<T, U> = (url: Extract<keyof T, string>, params: Record<string, unknown>) => Promise<U | undefined>;
// 业务请求 类型定义
type TRequest<T> = TCommonRequest<IApi, IResponseData<T>>;
// src/api/index.ts
// 定义 API 常量
const Api = {
  app: {
    list: '/app/list',
    detail: '/app/detail',
  },
};
// src/components/User.tsx

// 业务组件实现 数据请求功能代码
const request: TRequest<IUserData> = async (url, params) => {
  const res = await fetch(url, params);
  return res.json();
};

async function fetchData() {
  const user = await request(Api.app.list, {});
	// outcome log
  // {
  //  code: 0,
  //  result: { name: 'cat', age: 3 },
  //  message: '请求成功'
  // }
  console.log(user);
}

fetchData();

联合类型

联合类型的常用场景之一是通过多个对象类型的联合,实现手动的互斥属性,即这一属性如果有字段1,那就没有字段2:

interface IUser {
  info:
    | {
        vip: true;
        expires: string;
      }
    | {
        vip: false;
        promotion: string;
      };
}

declare let user:IUser;

if (user.info.vip) {
  console.log(user.info.expires); // ok
	console.log(user.info.promotion); // Error: Property 'promotion' does not exist on type '{ vip: true; expires: string; }'.ts(2339)
}

关键字

is

is 类型保护,用于判断类型的函数中做类型限制

// bad
function isString(value: unknown): boolean{
  return typeof value === "string";
}

//good
function isString(value: unknown): value is string{
  return typeof value === "string";
}

in

in 其实就像是遍历一样

type Keys = 'a' | 'b' | 'c';
type obj = { 
    [ T in Keys]: string;
}
// in 遍历 Keys,并为每个值赋予 string 类型
 
// type Obj = {
//     a: string,
//     b: string,
//     c: string
// }

keyof

keyof 可以获取一个对象接口的所有 key值

type obj = { a: string; b: string }
type Foo = keyof obj;
// type Foo = 'a' | 'b';

typeof

typeof 用于获取某个变量的具体类型

const obj = { a: '1' };
type Foo = typeof obj; 
// type Foo = { a: string }

extends、implements

  • extends用于接口与接口、类与类、接口与类之间的继承
  • implements用于类与类、类与接口之间的实现
    **注意: ** extends类似于es6的extends,implements没有继承效果的,但是要求子类上必须需有父类的属性和方法,更倾向于限制子类的结构!

infer

infer用于提取属性,具体的返回类型是依据三元表达式的返回而定。

type myInter<T> = T extends Array<infer U> ? U : T

Pick

用于在定义好的类型中取出特性的类型

interface UserInfo {
  id: string;
  name: string;
}
type NewUserInfo = Pick<UserInfo, 'name'>; // {name: string;}

Omit

用于在定义好的类型中去除特性的类型

interface UserInfo {
  id: string;
  name: string;
}
type NewUserInfo = Omit<UserInfo, 'name'>; // {id: string;}

Record

Record 可以获得根据 K 中所有可能值来设置 key 以及 value 的类型

interface UserInfo {
  id: string;
  name: string;
}
type CurRecord = Record<'a' | 'b' | 'c', UserInfo>; // { a: UserInfo; b: UserInfo; c: UserInfo; }

Partial、DeepPartial、Required

  • Partial 功能是将类型的属性变成可选

    interface UserInfo {
        id: string
        name: string
    }
    // bad
    const machinist: UserInfo = {
        name: 'machinist'
    } 
    // error 类型 "{ name: string; }" 中缺少属性 "id",但类型 "UserInfo" 中需要该属性。ts(2741)
    
    type NewUserInfo = Partial<UserInfo>;
    // good
    const machinist: UserInfo = {
        name: 'machinist'
    }
    

    **注意: ** Partial只支持处理第一层的属性,如果想要处理多层,可以使用DeepPartial,使用方法与Partial相同这里就不举例了

Partial的源码,非常简单,自己就可以实现一个简易版
type Partial<T> = {
    [P in keyof T]?: T[P];
};

Required (必选的)

  • Required 功能与Partial相反,将类型的属性变成必选
interface UserInfo {
  id?: string
  name?: string
}
type newUserInfo =  Required<UserInfo>
const machinist: newUserInfo = {
 id:"111"
}
// error 类型 "{ id: string; }" 中缺少属性 "name",但类型 "Required<UserInfo>" 中需要该属性。ts(2741)

Readonly (转化只读)

Readonly 就是为类型对象每一项添加前缀 Readonly

interface Person {
    readonly name: string; // 只有这一项有readonly
    age: number;
    id?: number;
}

// 使用方法
const newObj: Readonly<Person> = {
    name: '张三',
    age: 1,
    id: 1
};
// newObj.name = '李四'; // 异常 因为有readonly只读属性,只能初始化声明,不能赋值。

// Readonly<Person> 等同于 NewPerson
interface NewPerson {
    readonly name: string;
    readonly age: number;
    readonly id?: number;
}
Readonly的源码实现也非常简单
/**
 * Make all properties in T readonly
 */
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

ReturnType

ReturnType 用来获取函数的返回值的类型

type Func = (value: number) => string;
const foo: ReturnType<Func> = "1";

TS中的内置条件类型:ReturnType

先说一下条件类型是什么

image.png

  1. 条件类型是高级类型的一种。
  2. 条件类型是一种由条件表达式所决定的类型。
  3. 条件类型使类型具有了不唯一性,同样增加了语言的灵活性。

总言之,条件类型就是在类型中添加条件分支,以支持更加灵活的泛型,满足更多的使用场景。

例如:

T extends U ? X : Y

表示若类型T可被赋值给类型U,那么结果类型就是X类型,否则就是Y类型。

内置条件类型则是TS内部封装好的一些类型处理,使用起来更加便利。

内置条件类型:ReturnType

在 2.8 版本中,TypeScript 内置了一些与 infer 有关的映射类型,就比如说我们今天的主角:ReturnType<Type>

其用于提取函数的返回值类型:

Constructs a type consisting of the return type of function Type.

手撕示例:

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

ReturnType<T> 只是将 infer P 从参数位置移动到返回值位置,因此此时 P 即是表示待推断的返回值类型。

// 比如
type Func = () => User;
type Test = ReturnType<Func>; // Test = User

// 其他例子
type T0 = ReturnType<() => string>; // string
type T1 = ReturnType<<T>() => T>; // unknown
type T2 = ReturnType<<T extends U, U extends number[]>() => T>; // number[]

非法的例子:

type T = ReturnType<string>;
// Type 'string' does not satisfy the constraint '(...args: any) => any'.
type T = ReturnType<Function>;
// Type 'Function' does not satisfy the constraint '(...args: any) => any'.
// Type 'Function' provides no match for the signature '(...args: any): any'.

以上均不满足(...args: any): any'.,type T 将被视为any处理。

其他内置的条件类型还有:

Exclude<T, U> --T中剔除可以赋值给U的类型。
Extract<T, U> -- 提取T中可以赋值给U的类型。
NonNullable<T> --T中剔除null和undefined。
InstanceType<T> -- 获取构造函数类型的实例类型。

讲回infer

infer 最早出现在此 PR 中,表示在 extends 条件语句中待推断的类型变量。

示例如下:

type ParamType<T> = T extends (arg: infer P) => any ? P : T;

在这个条件语句 T extends (arg: infer P) => any ? P : T 中,infer P 表示待推断的函数参数。

整句表示为:如果 T 能赋值给 (arg: infer P) => any,则结果是 (arg: infer P) => any 类型中的参数 P,否则返回为 T

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

type Func = (user: User) => void;

type Param = ParamType<Func>; // Param = User
type AA = ParamType<string>; // string