半⼩时快速上⼿TypeScript类型编程!(附⼿摸⼿实战案例)1. Why
在介绍什么叫 TypeScript 类型编程和为什么需要学习 TypeScript 类型编程之前,我们先看⼀个例⼦,这⾥例⼦⾥包含⼀个 promisify 的函数,这个函数⽤于将 NodeJS 中 callback style 的函数转换成 promise style 的函数。
import * as fs from "fs";
function promisify(fn) {
return function(...args) {
return new Promise((resolve, reject) => {
fn(...args, (err, data) => {
if(err) {
return reject(err);
}
resolve(data);
});
});
}
}
(async () => {
let file = await adFile)("./xxx.json");
})();
如果我们直接套⽤上述的代码,那么 file 的类型和 adFile)(...) 中 (...) 的类型也会丢失,也就是我们有两个⽬标:
1. 我们需要知道 adFile)(...) 这⾥能够接受的类型。
2. 我们需要知道 let file = await ... 这⾥ file 的类型。
这个问题的答案在实战演练环节会结合本⽂的内容给出答案,如果你觉得这个问题简单得很,那么恭喜你,你已经具备本⽂将要介绍的⼤部分知识点。如何让类似于 promisify这样的函数保留类型信息是“体操”或者我称之为类型编程的意义所在。
2. 前⾔ (Preface)
最近在国内的前端圈流⾏⼀个名词“TS 体操”,简称为“TC”,体操这个词是从 Haskell 社区来的,本意就是⾼难度动作,关于“体操”能够实现到底多⾼难度的动作,可以参照下⾯这篇⽂章。
1.
不过笔者认为上述概念在前端圈可能⽐较⼩众、“体操”这个名字对于外⾏⼈来说相对难以与具体的⾏为对应起来、⽬前整个 TC 过程更像有趣的 brain teaser[2],所以笔者觉得 TC “体操”还是⽤ Type Computing 、Type Calculation 或者“类型编程”来记忆会⽐较好理解,这也容易与具体⾏为对应,本⽂在接下来的环节会⽤“类型编程”来取代“体操”说法。
3. 建模 (Modeling)
其实类型编程说⽩了就是写程序,这个程序接受类型作为输⼊,然后输出另⼀个类型,因此可以把它建模成写普通的程序,并按照⼀般计算机语⾔的组成部分对 TS 的类型相关语法进⾏归类。
4. 语法分类 (Grammar Classification)
⾸先我们看看基本的语⾔都有哪些语法结构,以 JS 为例,从 AST(抽象语法树)的⾓度来看[3],语法可以按照以下层级结构进⾏分类:
但是我们今天不会以这种从上到下的树状结构来整理和学习,这样⼦的学习曲线⼀开始会⽐较陡峭,所以作者并没有按照从上到下的顺序来整理,⽽是以学习普通语⾔的语法顺序来整理。
4.1 基本类型 (Basic Types)
类似于 JS ⾥⾯有基本类型,TypeScript 也有基本类型,这个相信⼤家都很清楚,TypeScript 的基本类型如下:Boolean[4]
Number[5]
String[6]
Array[7]
Tuple[8] (TypeScript 独有)
Enum[9] (TypeScript 独有)
Unknown[10] (TypeScript 独有)
Any[11] (TypeScript 独有)
Void[12] (TypeScript 独有)
Null and Undefined[13]
Never[14] (TypeScript 独有)
Object[15]
任何复杂类型都是基本类型的组合,每个基本类型都可以有具体的枚举:
type A = {
attrA: string,
attrB: number,
attrA: true, // Boolean 的枚举
...
}
4.2 函数 (Function)
类⽐ let func = (argA, argB, ...) => expression;
Javascript 中有函数的概念,那么 TypeScript 的 Type-level programming(以下简称 TP) 相关语法中有没有函数的概念呢?答案是有的,带范型的类型就相当于函数。
// 函数定义
type B<T> = T & {
attrB: "anthor value"
}
// 变量
class CCC {
...
}
type DDD = {
...
}
// 函数调⽤
type AnotherType = B<CCC>;
type YetAnotherType = B<DDD>;
其中 <T> 就相当于函数括弧和参数列表,= 后⾯的就相当于函数定义。或者按照这个思路你可以开始沉淀很多⼯具类 TC 函数了,例如
// 将所有属性变成可选的
type Optional<T> = {
[key in keyof T]?: T[key];
}typeof array
// 将某些属性变成必选的
type MyRequired<T, K extends keyof T> = T &
{
[key in K]-?: T[key];
};
// 例如我们有个实体
type App = {
_id?: string;
appId: string;
name: string;
description: string;
ownerList: string[];
createdAt?: number;
updatedAt?: number;
};
/
/ 我们在更新这个对象/类型的时候,有些 key 是必填的,有些 key 是选填的,这个时候就可以这样⼦⽣成我们需要的类型
type AppUpdatePayload = MyRequired<Optional<App>, '_id'>
上⾯这个例⼦⼜暴露了另外⼀个可以类⽐的概念,也就是函数的参数的类型可以⽤ <K extends keyof T> 这样的语法来表达。TypeScript 函数的缺陷 (Defect)
⽬前下⾯这三个缺陷笔者还没有到办法克服,聪明的你可以尝试看看有没有办法克服。
⾼版本才能⽀持递归
4.1.0 才⽀持递归
函数不能作为参数
在 JS ⾥⾯,函数可以作为另外⼀个函数的⼊参,例如:
function map(s, mapper) { return s.map(mapper) }
map([1, 2, 3], (t) => s);
但是在类型编程的“函数”⾥⾯,暂时没有相关语法能够实现将函数作为参数传⼊这种形式,正确来说,传⼊的参数只能作为静态值变量引⽤,不能作为可调⽤的函数。
type Map<T, Mapper> = {
[k in keyof T]: Mapper<T[k]>; // 语法报错
}
⽀持闭包,但是没有办法修改闭包中的值
TypeScript 的“函数中”⽬前笔者没有到相关语法可以替代
type ClosureValue = string;
type Map<T> = {
[k in keyof T]: ClosureValue; // 笔者没有到语法能够修改 ClosureValue
}
但是我们可以通过类似于函数式编程的概念,组合出新的类型。
type ClosureValue = string;
type Map<T> = {
[k in keyof T]: ClosureValue & T[k]; // 笔者没有到语法能够修改 ClosureValue
}
4.3 语句 (Statements)
在 TypeScript 中能够对应语句相关语法好像只有变量声明语句相关语法,在 TypeScript 中没有条件语句、循环语句函数、专属的函数声明语句(⽤下述的变量声明语句来承载)。
变量声明语句 (Variable Declaration)
类⽐:let a = Expression;
变量声明在上⾯的介绍已经介绍过,就是简单地通过 type ToDeclareType = Expresion 这样⼦的变量名加表达式的语法来实现,表达式有很多种类,我们接下来会详细到介绍到,
type ToDeclareType<T> = T extends (args: any) => PromiseLike<infer R> ? R : never; // 条件表达式/
带三元运算符的条件表达式
type ToDeclareType = Omit<App>; // 函数调⽤表达式
type ToDeclareType<T>= { // 循环表达式
[key in keyof T]: Omit<T[key], '_id'>
}
4.4 表达式 (Expressions)
带三元运算符的条件表达式 (IfExpression with ternary operator)
类⽐:a == b ? 'hello' : 'world';
我们在 JS ⾥⾯写“带三元运算符的条件表达式”的时候⼀般是 Condition ? ExpressionIfTrue : ExpressionIfFalse 这样的形式,在TypeScript 中则可以⽤以下的语法来表⽰:
type TypeOfWhatPromiseReturn<T> = T extends (args: any) => PromiseLike<infer R> ? R : never;
其中 T extends (args: any) => PromiseLike<infer R> 就相当条件判断,R : never 就相当于为真时的表达式和为假时的表达式。
利⽤上述的三元表达式,我们可以扩展⼀下 ReturnType,让它⽀持异步函数和同步函数
async function hello(name: string): Promise<string> {
solve(name);
}
// type CCC: string = ReturnType<typeof hello>; doesn't work
type MyReturnType<T extends (...args) => any> = T extends (
...args
) => PromiseLike<infer R>
R
: ReturnType<T>;
type CCC: string = MyReturnType<typeof hello>; // it works
函数调⽤/定义表达式 (CallExpression)
类⽐:call(a, b, c);
在上述“函数”环节已经介绍过
循环相关 (Loop Related)(Object.keys、Array.map等)
类⽐:for (let k in b) { ... }
循环实现思路 (Details Explained )
TypeScript ⾥⾯并没有完整的循环语法,循环是通过递归来实现的,下⾯是⼀个例⼦:
注意:递归只有在 TS 4.1.0 才⽀持
type IntSeq<N, S extends any[] = []> =
S["length"] extends N ? S :
IntSeq<N, [...S, S["length"]]>
理论上下⾯介绍的这些都是函数定义/表达式的⼀些例⼦,但是对于对象的遍历还是很常见,⽤于补全循环语句,值得单独拿出来讲⼀下。对对象进⾏遍历 (Loop Object)
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论