前言
TS能起到错误前置的作用,但是不正确地使用TS就不能保证,比如初学者经常会写any甚至会放任一些TS error,这不在讨论范围内。我们要讨论的是即使你能做到不写any,没有任何TS error,看上去很安全的代码,真的就能起到TS的错误前置,避免运行时报错导致的组件挂掉甚至页面白屏的作用吗?
答案当然是否定的,我们下面看看经典的不正确使用TS的案例。
1.objArr[index].key
// 危险的
interface IName {
name: string;
}
let names: IName[] | undefined;
// 这样是不会标红的,但是实际运行时很可能会报错,因为显然obj?.[2]可能是undefined类型。
names?.[2].name;
这其实是很危险的,因为一旦报错页面白屏是不可接受的。
那么如何避免呢?就是对象数组在下标取值后用一个变量表示而不是直接取值。
// 安全的
interface IName {
name: string;
}
let names: IName[] | undefined;
const thirdName = names?.[2];
// 这样会标红,并且正常选name字段VSCode会帮你自动补上?
thirdName.name
2.obj.key.grandsonKey操作时,key的类型声明信任了外界传入的值是在key list范围内导致的问题
const object = {
a: {
c: 'c'
},
b: {
c: 'c'
},
};
// key是外界传来的参数,我们错误地信任了key只可能是'a'或'b'
function foo(key: keyof typeof object) {
const d = object[key].c;
}
// @ts-ignore
// 然后调用方却传了个'c',于是在foo函数第一行就gg了整个作用域内的代码挂掉,而且如果没有catch住错误,整个页面都会白调。
foo('c');
不过,仅仅指出了问题是不够的,这种情况如何避免呢?这个方案仅供参考,并不是最优解。
interface IC {
c: string;
}
const object: Record<string, IC | undefined> = {
a: {
c: 'c'
},
b: {
c: 'c'
},
};
// key是外界传来的参数, 我们不信任它, 从类型上就兼容undefined或其他情况
function foo(key?: string) {
const d = key ? object[key]?.c : 'default';
}
foo('c');
3.obj.key.grandsonKey操作时,key信任了外界必传导致的问题
interface Iter {
key: {
grandsonKey: string;
};
}
// object是外界传进来的参数,我们错误地信任了key是必传参数
function foo(object: Iter) {
const a = object.key.grandsonKey;
}
// 实际传的数据, 造成
const object = {} as Iter;
foo(object);
进一步思考
在例子2中,我这的解决方案是把类型定的范围更大来让我们的代码没有处理这么大范围时会标红,这样确实能解决很关键问题,即运行时报错。
要知道运行时报错是很严重的问题,如果你没有捕获错误,或者用errorBoundary设置错误边界就会导致页面白屏,如果影响面大这就是P0事故了(即使上游异常数据的制造者的锅会大一些)。
即使有errorBoundary,但是如果说一个组件直接展示了组件出错了这种default文案,也并不能说是精细的错误处理,最理想的错误处理我们要考虑这个异常数据是不是可以兼容的,比如给个默认值,保证整个程序正常运作。
而话说回来,我们用一个更大范围的类型定义'a' | 'b' → string | undefined 确实可以让TS对未兼容代码提出错误,从而促使我们把异常数据转为正常数据,保证程序正常运转而不是抛出错误。
但是这个方案却被大佬否了,因为这样做在解决问题的同时又带来了另一个问题就是调用者不知道我该传什么了。
比如例子2中的foo函数我们暴露给业务方使用,业务方本来知道他应该传'a' | 'b'类型的参数,但是在我们因为不信任参数改为string | undefined 类型后,调用者只知道要传string,甚至似乎不传也行,但是实际上呢,不传仅仅是兼容了,能跑,但是绝对不会得到预期的结果的。
所以我的最新思路是这种要暴露给外界使用的场景,要搞一个接入层,接入层的功能是提供正常的接口定义,让调用者可以知道函数的设计意图。
在接入层并不定义处理函数,而是引入并调用兼容层的函数,并且参数直接透传原数据。
在兼容层我们的函数参数采用完全不信任的策略,如每条属性都设为非必传的、不枚举,都设为字符串等。这样能保证我们在使用时会做充分的兼容。
在消费层我们真正定义处理函数,接受的参数和接入层一致,因为异常数据都已经被兼容层处理了,所以可以放心使用。
当然这只是一个初步设想,还没有真正落地,欢迎大家指出自己的看法或者给出更好的解决方案。