这是我参与「第五届青训营 」伴学笔记创作活动的第 10 天
类型守卫 & 类型保护
类型保护又叫类型守卫,是为了提高程序的鲁棒性存在的。先来看一个例子:
`enum Type { JavaScript , Python };
class JavaScript {
helloJS(){
console.log("hello JavaScript!");
}
}
class Deno {
helloDeno(){
console.log("hello deno!");
}
}
function getLangage(type:Type):void{
const lang = type === 0 ? new JavaScript() : new Deno();
if(lang.helloJS){//报错
lang.helloJS();
}else{
lang.helloDeno();
}
console.log(lang);
}
getLangage(Type.JavaScript);`
上面的代码 lang 使用三元来赋值的,lang 是一个联合类型,const lang: JavaScript | Deno 无法准确确定 lang 的值,所以条件语句里的代码为了防止出错,应为使用类型断言。条件语句的代码改写为:
if((lang).helloJS){
(lang).helloJS();
}else{
(lang).helloDeno();
}
现在没问题了,但是每个地方我们都加了类型断言,很明显这种方案不是我们想要的可读性差写起来也是很麻烦类型守卫就是解决这个问题的。可以简单理解为类型保护就是解决类型断言可读性差和书写不方便的。
类型保护:能够在特定的区块中(比如函数里)保证变量属于某种特定的类型,可以在区块中放心的使用此类型的属性或方法。
下面介绍四种,还使用上面的案例,只改动条件判断语句:
instanceof
if(lang instanceof JavaScript){
lang.helloJS();
}else{
lang.helloDeno();
}
使用 in ,属性名是否在对象中
enum Type { JavaScript , Python };
class JavaScript {
JS:any;
helloJS(){
console.log("hello JavaScript!");
}
}
class Deno {
Py:any;
helloDeno(){
console.log("hello deno!");
}
}
function getLangage(type:Type):void{
const lang = type === 0 ? new JavaScript() : new Deno();//const lang: JavaScript | Deno
if("JS" in lang){
lang.helloJS();
}else{
lang.helloDeno();
}
console.log(lang);
}
getLangage(Type.JavaScript);
typeof 检测基本类型的方法
function main(x:string | number){
if(typeof x === "string"){
console.log("x is string");
}else {
console.log("x is number");
}
}
main(12);
类型保护函数
要定义一个类型守卫,我们只要简单地定义一个函数,它的返回值是一个类型谓词:
enum Type { JavaScript , Python };
class JavaScript {
JS:any;
helloJS(){
console.log("hello JavaScript!");
}
}
class Deno {
PY:any;
helloDeno(){
console.log("hello JavaScript!");
}
}
function getLangage(type:Type):void{
const lang = type === 0 ? new JavaScript() : new Deno();//const lang: JavaScript | Deno
if(ISJS(lang)){
lang.helloJS();
}else{
lang.helloDeno();
}
console.log(lang);
}
getLangage(Type.JavaScript);
function ISJS(lang : JavaScript | Deno):lang is JavaScript{
return (lang).helloJS !== undefined;
}
在这个例子里,lang is JavaScript 就是类型谓词。 谓词为 parameterName is Type 这种形式,parameterName必须是来自于当前函数签名里的一个参数名。
索引类型(Index types)
索引类型其实也不算一个类型,其实就是一个函数,现在我们写出以下代码:
var obj = {
a:1,
b:2,
c:3,
}
function getValues(obj:any,keys:string[]){
return keys.map(key=>obj[key]);
};
console.log(getValues(obj,["a","b"])); //[1,2]
从一个对象里面来获取属性的子集,getValues(obj,["a","b"] 能够得到期望的结果,但是如果我们通过getValues(obj,["d"] 去得到对象属性 d 的值,很遗憾的是 undefined ,这没啥问题,但是别忘了 TypeScript 是强类型语言,照理来讲我们 getValues(obj,["d"] 去调用一个对象里面不存在的属性,应该报错才是符合强类型语言的思路,索引类型就是为了这个目标出现的。
先来了解几个前景知识:
索引类型查询操作符:keyof T
interface Person {
name: string;
age : number;
}
let personProps: keyof Person; // 'name' | 'age'
对于任何类型 T,keyof T 的结果为 T 上已知的公共属性名的联合。keyof Person 是完全可以与 'name' | 'age'互相替换的。 不过不同的是如果你添加了其它的属性到 Person,例如 address: string,那么 keyof Person 会自动变为 'name' | 'age' | 'address'。 你可以在像 getValues 函数这类上下文里使用 keyof,因为在使用之前你并不清楚可能出现的属性名, 但编译器会检查你是否传入了正确的属性名给 getValues。这句话后面解释。
索引访问操作符:T[K]:
interface Person {
name: string;
age : number;
}
let personProps: keyof Person; // 'name' | 'age'
// 索引访问操作符
let value:Person["name"];//value:string;
T[K] 只不过是拿到对象属性值的类型。
现在来实现,我们要对输入输出进行约束,首先想到的应该是泛型函数,第二个参数又是数组,对应的应该使用泛型数组函数:
var obj = {
a:1,
b:2,
c:3,
}
function getValues(obj:T,keys:K[]):K[]{
return keys.map(key=>obj[key]);//报错,因为数组里面的元素可以是任意类型
};
obj[key] 报错因为 keys ,是个数组结合泛型使用,数组里面的元素会被翻译成任意类型,number 就是其中一种,而对象的属性是不允许为 number 所以报错。这时这句话就有用了:
你可以在像 getValues 函数这类上下文里使用 keyof,因为在使用之前你并不清楚可能出现的属性名,
但编译器会检查你是否传入了正确的属性名给 getValues。这句话后面解释。
这时用到泛型约束(extends)结合索引类型查询操作符:
//K extends keyof T 表示,现在 K 是 `a/b/c/x` 并不是类型了,所以T[K]
function getValues(obj:T,keys:K[]):K[]{
return keys.map(key=>obj[key]);//报错返回值类型不对
};
console.log(getValues(obj,["a","b"])); //[1,2]
原因是现在的 K 不是类型,所以用到索引访问操作符:T[K]正确的写法:
function getValues(obj:T,keys:K[]):T[K][]{
return keys.map(key=>obj[key]);//报错,因为数组里面的元素可以是任意类型
};
console.log(getValues(obj,["a","b"])); //[1,2]
到此大功告成,在输入对象里面没有出现的属性就会报错:
getValues(obj,["d"]);//Type 'string' is not assignable to type '"a" | "b" | "c"'.ts(2322)