Typescript竭尽所能,把运行时异常转移到编译时。Typescript是功能丰富的系统,加上强大的静态和符号分析能力,包揽了大量辛苦的工作。
但是有些问题是无法避免的,比如网络和文件系统异常,解析用户输入时出现额错误,堆栈溢出及内存不足。不过,Typescript的类型系统足够强大,提供了很多处理运行时错误的方式,不会眼看程序崩溃。
function ask() {
return prompt("when is your birthday")
}
function isValid(data: Date) {
return Object.prototype.toString.call(date) === '[object Date]' &&
!Number.isNaN(date?.getTime())
}
function parse(birthday: string | null): Date | null {
let date;
// null判断
if (birthday) {
date = new Date(birthday)
if (!isValid(date)) {
return null
}
}
return new Date
}
let date = parse(ask())
// 强制判断是否为null
if (date) {
console.info("Date is ", date.toISOString());
} else {
console.error("Error parsing date for some reason");
}
返回null是处理错误最轻量的方式。
然而这么做丢失了一些信息,调用方不知道为什么错误。
把返回null改成抛出异常
function parse(birthday:string):Date{
let date = new Date(birthday)
if(!isValid(date)){
throw new RangeError("enter a date in the form YYYY/MM/DD")
}
return date
}
//使用时捕获错误
try{
let date = parse(ask())
log(date.toISOString())
}catch(e){
if(e instanceof RangeError){
log(e.message)
}else{
throw e
}
}
自定义错误类型
class InvalidDateFormatError extends RangeError{}
class DateIsInTheFutureError extends RangeError{}
function parse(birthday:string):Date{
let date = new Date(birthday)
if(!isValid(date)){
throw new InvalidDateFormatError("enter a date in the form YYYY/MM/DD")
}
if(date.getTime()>Date.now()){
throw new DateIsInTheFutureError("are you a timelord")
}
return date
}
Typescript不支持throws子句。不过我们可以使用并集类型近似实现这个特性
throws子句的作用是指出一个方法可能抛出什么运行时异常,让使用方知道该处理那些异常
class InvalidDateFormatError extends RangeError { }
class DateIsInTheFutureError extends RangeError { }
/**
@thorws{InvalidDateFormateError} xxxx
**/
function parse(birthday: string|null): Date | InvalidDateFormatError | DateIsInTheFutureError {
let date = new Date(birthday!)
if (!isValid(date)) {
return new InvalidDateFormatError("enter a date in the form YYYY/MM/DD")
}
if (date.getTime() > Date.now()) {
return new DateIsInTheFutureError("are you a timelord")
}
return date
}
let result = parse(ask())
// 手动判断可能出现的异常
if(result instanceof InvalidDateFormatError){
console.log("");
}else if(result instanceof DateIsInTheFutureError){
console.log("----");
}else{
console.log("ok");
}
这里,我们利用Typescript的类型系统实现了
使用方太烂的话,可以不用逐一处理各个异常,但是要明确写出来
if(result instanceof Error){
log
}else{
log
}
这种方式的缺点是,串联和嵌套可能会让人觉得烦躁。如果一个函数返回T|Error1,那么使用该函数的函数有两个选择:
function x():T|Error1{}
function y():U|Error1|Error2{
let a = x()
if(a instanceof Error){
return a
}
}
function z():U|Error1|Error2|Error3{
let a = y()
if(a instanceof Error){
return a
}
}
除此之外,还可以使用专门的数据类型描述异常。这种方式与返回值和错误的并集相比是有缺点的(尤其是与不使用这些数据类型的代码互操作时),但是却便于串联可能出错的操作。在这方面,常用的三个选项是Try(try type),Option(也叫Maybe)和Either(either type)类型。本章只介绍Option类型,其他两个类型本质上基本相同。
注意:Try,Option,和Either与Array,Error,Map或Promise不同,不是JavaScript环境内置的数据类型。如果想使用,要在NPM中寻找实现,或者自己编辑
Option类型源自Haskell,OCaml,Scala和Rush等语言,隐含的意思是,不返回一个值,而是返回一个容器,该容器里可能有一个值,也可能没有。 这个容器有一些方法,即使没有值也能串联操作。容器几乎可以是任何数据结构,只要能在里面存放值。例如,可以使用数组作为容器:
function parse(birthday: string | null): Date[] {
if (birthday) {
let date = new Date(birthday)
if (!isValid(date)) {
return []
}
return [date]
}
return []
}
function isValid(date: Date) {
return Object.prototype.toString.call(date) === '[object Date]' &&
!Number.isNaN(date?.getTime())
}
let ask = (): string | null => {
let number = Math.random();
if (number > 0.5) {
return "2023/08/01"
} else {
return null
}
}
let date = parse(ask())
date
.map(_=>_.toISOString())
.forEach(_=>console.log("date is",_))
你可能注意到了,Option的缺点与返回null一样,只告诉使用方什么地方出错了,而未说明出错的原因。
Option真正发挥作用是在一次执行多个操作,而每个操作都有可能出错。
例如,我们之前一定假定ask一定成功,而parse可能失败。但是如果ask失败了,我们不能放置不理,继续计算,此时,仍然可以使用Option。
let ask = () => {
let result = "2023/08/01"
if (result === null) {
return []
}
return [result]
}
ask()
.map(parse)
.map(date=>date.toISOString())// 报错
.forEach(date=>console.log("date is",date))
出错了。这是因为我们把一个Date数组(Date[])映射到了一个Date数组构成的数组上(Date[][]
),解决方法是在操作之前整平数组:
let ask = () => {
let result = "2023/08/01"
if (result === null) {
return []
}
return [result]
}
flatten(ask()
.map(parse))
.map(date=>date.toISOString())// 报错
.forEach(date=>console.log("date is",date))
function flatten(array:T[][]):T[]{
return Array.prototype.concat.apply([],array)
}
这样有点不便,Option类型没有告诉我们什么信息(一切都是常规数组)。为了改变这种局面,我们可以把整个过程(把一个值放入一个容器,提供操作那个值的方式,提供从容器中取回结果的方式)包装成一个特殊的数据结构,让一切一目了然。实现好以后,可以像下面这样使用
ask()
.flatMap(parse)
.flatMap(date => new Some(date.toISOString()))
.flatMap(date => new Some("date si "+date))
.getOrElse("Error parsing date for some reason")
我们将才用下述方式定义Option类型:
interface Option{} //1.
class Some implements Option {//2.
constructor(private value:T){}
}
class None implements Option{}//3
这几个类型等效于下述通过数组实现的Option:
我们能对Option做些什么呢,就目前
串联操作可能为空的Option
从Option取得值
首先在Option接口中定义这两个操作,然后在Some和None中具体实现:
interface Options {
flatMap(f: (value: T) => None): None
flatMap(f: (value: T) => Options): Options
getOrElse(value: T): T;
}
class Some implements Options {
constructor(private value: T) {}
flatMap(f: (value: T) => None): None
flatMap(f: (value: T) => Some): Some
flatMap(f: (value: T) => Options): Options {
return f(this.value);
}
getOrElse(): T {
return this.value;
}
}
class None implements Options {
flatMap(): None {
return this;
}
getOrElse(value: U): U {
return value;
}
}
function Options(value:null|undefined):None
function Options(value:T):Some
function Options(value:T):Options{
if(value == null){
return new None
}
return new Some(value)
}
ask()
.flatMap(parse)
.flatMap(date => new Some(date.toISOString()))// Options
.flatMap(date => new Some("date si " + date))// Options
.getOrElse("Error parsing date for some reason")// string
// let result = Options(6)
// .flatMap(n=>Options(n*3))
// .flatMap(n=>new None)
// .getOrElse(7)
Option也不是没有缺点,Option通过一个None表示失败,没有关于失败的详细信息,也不知道失败的原因。另外与不使用Option的代码无法互操作(要自己手动包装API,让他们返回Option)