精进TypeScript--你了解any类型吗?

TypeScript中的any类型有效地静默了类型检查器和Typescript语言服务。它会掩盖真正的问题,有损于开发者体验,并破坏开发者对类型系统的信心。因此,尽量避免使用它!

TypeScript的类型系统是渐进和可选的:渐进是指你可以一项一项地将类型添加到代码中,可选是指你可以随时禁用类型检查器。这些功能的关键是any类型。
看个示例:

let age: number;
age = '12'; // error: 不能将字符串12分配给类型number
age = '12' as any; // 这样OK

我们刚开始使用TypeScript,如果遇到一个不理解的错误、认为类型检查器是不正确的或者只是不想花时间写出类型声明时,使用any类型和类型断言(as any)确实非常诱人。在某些情况下,这也许可行,但是any消除了许多使用TypeScript的优点。所以,使用any前,你至少应该了解它的危险性。

any类型的缺点

  • any类型没有类型安全
    如上例子,类型说明中age是一个number,但any让你可以给它分配一个string,而类型检查器会相信它就是一个number,这样,下面的结果就会超出我们的预期:
age += 1; // OK,运行时,结果为“121”
  • any类型会让你打破契约
    我们编写一个函数,是在指定一个契约:如果调用者给你一个特定类型的输入,你将产生一个特定类型的输出。但是any可以打破这些契约。
functiong calculateAge(birthDate: Date): number {
	// ...
}
let birthDate: any = '1990-01-01';
calculateAge(birthDate); // OK

参数birthDate应该是一个Date,而不是一个string。而any在这里打破了calculateAge的契约。这会很麻烦,因为JavaScript经常在类型之间进行隐式转换,有时一个string也会在该是number的地方正常运行。

  • any类型没有语言服务
    当一个符号有一个类型时,TypeScript的语言服务能否提供自动不全和上下文文档。对于带有any类型的符号,你只能靠自己了。
    TypeScript的座右铭是“规模化的JavaScript”。“规模化”的一个关键部分是语言服务,这是Typescript体验的核心部分。丧失它意味着生产力的损失,这不仅是对你而言,更是对工作在你的代码上的其他人而言。
  • any类型会掩盖重构代码的错误
    示例:假设有个下拉选择框,用户可以选择某些选项(Item),你的一个组件可以有一个onSelectItem回调函数。为一个选项写一个类型似乎很麻烦,所以你用any作为一个代替:
interface ComponentProps {
	onSelectItem: (item: any) => void;
}

function renderSelector(props: ComponentProps) { /*...*/ }
let selectedId: number = 0;
function handleSelectItem(item: any) {
	selectedId = item.id;
}
renderSelector({onSelectItem: handleSelectItem})

如果你重新设计了选择器,不用将整个item对象传递给onSelectItem,这没什么大不了,因为你只需要ID。你只改变了ComponentProps的函数签名:

interface ComponentProps {
	onSelectItem: (id: number) => void;
}

更新代码,一切都通过了类型检查器的检查。handleSelectItem接受了一个any参数,所以它对一个选项(Item)和一个ID一样都没问题。尽管通过了类型检查器,但它还是会产生一个运行时异常。如果你使用了一个更具体的类型,它将被类型检查器捕获。

  • any类型遮蔽了你的类型设计
    好的类型设计对于写出干净、正确和可理解的代码必不可少。对于一个any类型,你的类型设计是隐性的。这使得我们很难知道这个设计是否是一个好的设计,甚至根本不知道这个设计是什么。如果你的同事需要基于你的代码变更,他们就得重新构建你是否和如何改变了应用状态。因此最好把类型写出来让大家看到。

  • any类型破坏了你对类型系统的信心
    TypeScript的目的是让你的代码更简单、清晰,但是有很多any类型的Typescript可能比无类型的JavaScript更难处理,因为你必须修复各种类型错误,同时还得在脑海中记住真正的类型。如果你的代码的类型和事实相符,你就可以摆脱在脑海中保存类型信息的负担,TypeScript会为你记录它。

对于你必须使用any的情况,分别有更好的和更坏的方法来做到这一点。更多关于如何限制类型检查器的any坏处,请看下文。

和any一起工作

类型系统在传统上是二元对立的:一种语言要么有一个完全静态的类型系统,要么有一个完全动态的类型系统。TypeScript模糊了这一界限,因为它的类型是渐进和可选的,你可以将类型只添加到你的程序的其中一部分,而不用管另外的部分。
这对于将现有的JavaScript代码库逐步迁移到TypeScript是必不可少的。其中关键是any类型,它可以有效地禁用部分代码的类型检查。它既强大又容易被滥用。学会明智地使用any是编写高效的TypeScript的关键。

为any类型使用最窄的范围

  • 使any的作用范围尽可能狭窄,以避免在你的代码的其他地方发生不必要的类型安全损失
  • 永远不要从一个函数中返回一个any类型。这将导致任何调用该函数的客户端失去类型安全
  • 如果需要抑制一个错误,可以考虑使用@ts-ignore作为any的替代(但不建议)
function a(b: Bar) { /*...*/ }

function f() {
	const x = returnFoo();
	a(x); // x类型为“Foo”,不能分配给类型“Bar”
}
// 有两种方式强制Typescript接受这段代码
function f1() {
	const x: any = returnFoo(); // 不建议
	a(x); 
}
function f2() {
	const x = returnFoo(); 
	a(x as any); // 这样做
}

第二种方式是可取的。因为该any类型的范围是一个函数参数中的单一表达式。它在这个参数或这一行之外没有任何影响。而第一个例子中,x的类型是any,其作用范围直到函数结束。
如果你从这个函数返回x,风险会更大。因为any返回类型是有“传染性”的,它可以在整个代码库中传播。下面代码中any类型就悄悄地出现在g中了。这换作范围更窄的f2中的any就不会发生。

function f1() {
	const x: any = returnFoo();
	a(x); 
	return x;
}
function g() {
	const foo = f1(); // 类型是any
	foo.fooMethod(); // 调用没有被检查!
}

比起普通的any,选择更精确的any变体

  • 使用any时,请想一下是否任何JavaScript的值都真的被允许
  • 选择更精确的any形式,如any[] 或 {[id: string]: any} 或 () => any,如果它们能更准确地为你的数据建模
    any类型包含了JavaScript中所有可以表达的值,这是一个超大集合!不仅包含所有的数字和字符串,还包括所有的数组、对象、正则表达式、函数、类和DOM元素,还有null和undefined。
function getLengthBad(array: any) { // 别这样
	return array.length;
}

function getLength(array: any[]) { // 建议
	return array.length;
}

getLengthBad(/123/); // 没有错误,返回undefined
getLength(/123/); // 类型检查报错,RegExp不能赋给类型any[]

如果你希望参数是一个数组的数组,但并不关心具体类型,你可以使用any[][]。如果你期望是某种对象,但不知道是什么,可以使用{[key: string]: any}。
如果你只是想要一个函数类型,也要避免使用any。比如:

type Fn0 = () => any; // 无参数的可调用函数
type Fn1 = (arg: any) => any; // 一个参数
type Fnn = (...args: any[]) => any; // 有任意个参数

对未知类型的值使用unknown而不是any

  • unknown类型是any类型安全的替代方法。当你知道有一个值但不知道它的类型是什么时,请使用unknown。
  • 使用unknown以强制你的用户使用类型断言或进行类型检查
  • 了解{}、object和unknown的区别:
    {}类型由除null和undefined以外的所有值组成;
    object类型由所有非基本数据类型组成。不包括true/12/‘foo’,但是包括对象和数组;
    在unknown类型被引入之前,{}的使用是比较普遍的。现在是比较罕见的用法:只有在你真的知道null和undefined是不可能的情况下,才使用{}代替unknown

下面是个示例:
假设有一个YAML解析器。你的parseYAML方法的返回类型应该是什么?为了方便返回any很有吸引力。

function parseYAML(yaml: string): any {
	// ...
}

前面我们提到,避免使用“有传染性”的any类型,特别是不要从函数中返回。理想情况下,你希望你的用户能立即将结果分配给另一个类型:

interface Book {
	name: string;
	author: string;
}
const boot: Book = parseYAML(`
	name: TypeScript
	author: Bob
`)

如果没有类型声明,book变量就会悄悄地得到一个any类型,而且在任何使用它的地方都会阻碍类型检查。

const book = parseYAML(`
	name: JavaScript
	author: Charis
`)
alert(book.title); // 没错,运行时发出“undefined”警报
book('read'); // 没错,运行时抛出“TypeError: book is not a function”
// 一个更安全的选择是令parseYAML返回一个unknown类型
function safeParseYAML(yaml: string): unknown{
	return parseYAML(yaml);
}
const book = safeParseYAML(`
	name: JavaScript
	author: Charis
`);
alert(book.title); // 对象类型为‘unkonwn’
book('read'); // 对象类型为‘unkonwn’

从any可分配性的角度来思考,有助于理解unknown类型。any的力量和危险来自两个属性:1.任何类型都可以分配给any类型;2.any类型可分配给任何其他类型。
unknown类型是一个很适合类型系统的any替代品。它具有第一个属性(任何类型都可以被分配给unknown),但不具备第二个属性(unknown仅可分配给unknown,或any)。
试图用unknown类型访问一个值上的属性是错误的,试图调用它或用它做算术运算也是错误的。你用unknown做不了多少事情,这正是关键所在。关于unknown类型的错误会鼓励你添加一个合适的类型:

const book = safeParseYAML(`
	name: JavaScript
	author: Charis
`) as Book;
alert(book.title); // book上不存在属性title
book('read'); // 此表达式不被调用

这些错误是比较合理的。由于unknown不能分配给其他类型,所以才需要类型断言。但这也是合适的:我们确实比TypeScript更了解结果对象的类型。当你知道会有一个值,但不知道它的类型时,使用unknown类型就是合适的。

追踪你的类型覆盖率以防止类型安全中的回归问题

由于any类型会对类型安全和开发者体验造成负面影响,因此跟踪你的代码库中any类型的数量是个好主意。比如,npm中的type-coverage包:

$ npx type-coverage
9985 / 10117 98.69%
$ npx type-coverage --detail  // 每个any类型出现的位置
path/code.ts:1:10 getUserInfo
...

这意味着,你的代码中的10117个符号中,有9985个(98.69%)的类型不是any或any的别名。如果一个变化无意中引入了一个any类型,并且它流经你的代码,你会看到这个百分比有所下降。

  • 28
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

~卷心菜~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值