你好居住者!Dan Vanderkam的书对那些已经使用JavaScript和TypeScript的人来说将是最有用的。本书的目的不是要教育读者使用这些工具,而是帮助他们提高专业水平。阅读它之后,您将更好地了解TypeScript组件的工作方式,避免许多陷阱和陷阱,并提高自己的技能。参考指南将向您展示使用该语言完成同一任务的五种不同方式,而一本“有效”的书将解释哪种更好以及为什么更好。
书籍结构
这本书是短文集(规则)。规则分为主题部分(章节),可以根据感兴趣的问题自动访问。
每个规则标题都包含一个提示,因此请查看目录。例如,如果您正在编写文档,并且对是否要编写类型信息存有疑问,请参阅目录和规则30(“在文档中不要重复类型信息”)。
本书中几乎所有的结论都使用代码示例进行了演示。我认为您和我一样,倾向于阅读技术书籍,只看示例而不是文本部分。当然,我希望您仔细阅读说明,但我已涵盖了示例中的要点。
阅读每个技巧后,您可以确切了解如何以及为什么它将帮助您更有效地使用TypeScript。您还将了解在某些情况下它是否不可用。我记得有效C ++的作者斯科特·迈尔斯(Scott Myers)给出的一个例子:导弹软件开发人员在防止资源泄漏方面可能忽略了建议,因为当导弹击中目标时,他们的程序被破坏了。我不知道有没有用JavaScript编写的控制系统的火箭,但是James Webb望远镜上提供了这种软件。所以要小心
每个规则以“必须记住”块结尾。快速浏览一下,您可以大致了解材料并突出显示主要内容。但我强烈建议您阅读整个规则。
摘录。规则4.习惯结构化打字
JavaScript是无意鸭式的:如果您将具有正确属性的值传递给函数,则它不在乎如何获得该值。她只是使用它。TypeScript对这种行为进行建模,这有时会导致意外结果,因为验证者对类型的理解可能比您更广泛。开发结构化类型化的技巧将使您能够更好地感觉到真正存在错误的地方并编写更可靠的代码。
例如,您正在使用物理库,并且具有2D向量类型:
interface Vector2D {
x: number;
y: number;
}
您编写一个函数来计算其长度:
function calculateLength(v: Vector2D) {
return Math.sqrt(v.x * v.x + v.y * v.y);
}
并输入命名向量的定义:
interface NamedVector {
name: string;
x: number;
y: number;
}
该calculateLength函数将与NamedVector一起使用,因为它包含x和y属性,它们是数字。TypeScript理解这一点:
const v: NamedVector = { x: 3, y: 4, name: 'Zee' };
calculateLength(v); // ok, 5.
有趣的是,您没有在Vector2D和NamedVector之间声明关系。您也不必为NamedVector编写替代的calculateLength执行。TypeScript类型系统模拟JavaScript的运行时行为(规则1),该行为允许NamedVector基于与Vector2D相当的结构来调用calculateLength。因此,表达为“结构类型”。
但这也会导致问题。假设您添加了3D矢量类型:
interface Vector3D {
x: number;
y: number;
z: number;
}
并编写一个函数来标准化向量(使其长度等于1):
function normalize(v: Vector3D) {
const length = calculateLength(v);
return {
x: v.x / length,
y: v.y / length,
z: v.z / length,
};
}
如果调用此函数,则很可能会获得多个长度:
> normalize({x: 3, y: 4, z: 5})
{ x: 0.6, y: 0.8, z: 1 }
出了什么问题,为什么TypeScript没有报告错误?
问题在于,calculateLength适用于2D向量,而normalize适用于3D。因此,在标准化过程中将忽略z分量。
类型检查器没有抓住这一点似乎很奇怪。为什么允许在3D向量上调用calculateLength,即使其类型适用于2D向量?
与named搭配得很好的做法在这里适得其反。在{x,y,z}对象上调用calculateLength不会引发错误。因此,类型检查模块不会抱怨,最终导致出现错误。如果您希望在这种情况下检测到错误,请参考规则37。
编写函数时,很容易想象它们将由您声明的属性调用,而没有别的。这称为“密封”或“精确”类型,不能在TypeScript类型系统中应用。不管喜欢与否,这些类型在这里打开。
有时这会导致意外:
function calculateLengthL1(v: Vector3D) {
let length = 0;
for (const axis of Object.keys(v)) {
const coord = v[axis];
// ~~~~~~~ "any",
// "string"
// "Vector3D"
length += Math.abs(coord);
}
return length;
}
为什么这是一个错误?由于axis是Vector3D中的v键之一,因此它必须是x,y或z。并且根据原始的Vector3D声明,它们都是数字。因此,坐标类型也不应该是数字吗?
这不是一个错误的错误。我们知道Vector3D是严格定义的,没有其他属性。尽管他可以:
const vec3D = {x: 3, y: 4, z: 1, address: '123 Broadway'};
calculateLengthL1(vec3D); // ok, NaN
由于v可能具有任何属性,所以axis是string类型的。TypeScript没有理由将v [axis]视为一个数字。遍历对象时,可能难以实现正确的键入。我们将在规则54中回到该主题,但是现在我们不使用循环:
function calculateLengthL1(v: Vector3D) {
return Math.abs(v.x) + Math.abs(v.y) + Math.abs(v.z);
}
结构化类型也会导致类比较中可能出现的意外情况,以进行可能的属性分配:
class C {
foo: string;
constructor(foo: string) {
this.foo = foo;
}
}
const c = new C('instance of C');
const d: C = { foo: 'object literal' }; // ok!
为什么可以将d分配给C?它具有字符串的foo属性。它还有一个构造函数(来自Object.prototype),可以使用参数调用(尽管通常在不带参数的情况下调用它)。因此结构是相同的。如果您在C构造函数中有逻辑并编写一个函数来运行它,这可能会引起意外。这与C ++或Java之类的语言有显着差异,C ++或Java这样的语言声明C类型参数可确保它属于C或它的子类。
在编写测试时,结构化类型很有帮助。假设您有一个执行数据库查询并处理结果的函数。
interface Author {
first: string;
last: string;
}
function getAuthors(database: PostgresDB): Author[] {
const authorRows = database.runQuery(`SELECT FIRST, LAST FROM
AUTHORS`);
return authorRows.map(row => ({first: row[0], last: row[1]}));
}
为了测试它,您可以创建一个PostgresDB模拟。但是,更好的解决方案是使用结构化类型并定义更窄的接口:
interface DB {
runQuery: (sql: string) => any[];
}
function getAuthors(database: DB): Author[] {
const authorRows = database.runQuery(`SELECT FIRST, LAST FROM
AUTHORS`);
return authorRows.map(row => ({first: row[0], last: row[1]}));
}
您仍然可以将postgresDB传递到输出中的getAuthors函数,因为它具有runQuery方法。结构化类型并不强制PostgresDB报告其正在执行数据库。TypeScript会为您解决。
在编写测试时,您还可以传递一个更简单的对象:
test('getAuthors', () => {
const authors = getAuthors({
runQuery(sql: string) {
return [['Toni', 'Morrison'], ['Maya', 'Angelou']];
}
});
expect(authors).toEqual([
{first: 'Toni', last: 'Morrison'},
{first: 'Maya', last: 'Angelou'}
]);
});
TypeScript将确定测试数据库是否符合该接口。同时,您的测试根本不需要任何有关输出数据库的信息:不需要模拟库。通过引入抽象(DB),我们从执行细节(PostgresDB)中释放了逻辑。
结构化类型的另一个优点是,它可以明显打破库之间的依赖性。有关此主题的更多信息,请参见规则51。
请记住,
JavaScript使用鸭子类型,而TypeScript使用结构化类型对其进行建模。结果,分配给您的接口的值可能具有声明的类型中未指定的属性。TypeScript中的类型不是密封的。
请记住,类也要遵守结构化的打字规则。因此,您最终可能会得到与预期不同的课程样本。
使用结构化键入可以更轻松地测试项目。
规则5.限制任何类型的使用
TypeScript中的类型系统是渐进的和选择性的。渐进性体现在能够逐步向代码添加类型的能力,选择性体现在需要时禁用类型检查模块的能力。在这种情况下,控制的关键是任何类型:
let age: number;
age = '12';
// ~~~ '"12"' 'number'.
age = '12' as any; // ok
权利模块,指示错误,但是可以通过简单地添加asany来避免。当您开始使用TypeScript时,如果您不理解错误,不信任验证者或不想花时间拼写类型,那么使用任何类型或用作任何断言就会变得很诱人。但是请记住,任何方法都会抵消TypeScript的许多好处,即:
降低代码安全性
在上面的示例中,根据声明的类型,age是数字。但是任何允许将字符串分配给它。验证器将假定这是一个数字(即您声明的数字),这将导致混乱。
age += 1; // ok. age "121".
允许您违反条件
创建函数时,您可以设置一个条件,即从调用中接收到某种数据类型后,它将在输出中生成相应的类型,这种情况会被违反:
function calculateAge(birthDate: Date): number {
// ...
}
let birthDate: any = '1990-01-19';
calculateAge(birthDate); // ok
birthDate参数必须是Date,而不是字符串。any类型允许违反与calculateAge相关的条件。这可能特别有问题,因为JavaScript倾向于进行隐式类型转换。因此,在某些应该使用数字的情况下,字符串将失败,而在其他情况下则不可避免地将失败。
消除了对语言服务的支持为
字符分配类型后,TypeScript语言服务能够提供适当的自动替换和上下文文档(图1.3)。

但是,通过将any分配给字符,您必须自己做所有事情(图1.4)。

并重命名。如果您具有“人员”类型和用于格式化名称的函数:
interface Person {
first: string;
last: string;
}
const formatName = (p: Person) => `${p.first} ${p.last}`;
const formatNameAny = (p: any) => `${p.first} ${p.last}`;
然后可以在编辑器中首先突出显示并选择“重命名符号”以将其重命名为firstName(图1.5和图1.6)。
这将更改formatName函数,但不会发生任何变化:
interface Person {
first: string;
last: string;
}
const formatName = (p: Person) => `${p.firstName} ${p.last}`;
const formatNameAny = (p: any) => `${p.first} ${p.last}`;

»有关这本书的更多详细信息,可以在出版社的网站上找到
»目录
»摘录
用于居住者的优惠券可享受25%的折扣-TypeScript
在为该书的纸质版本付款后,会向该电子邮件发送一本电子书。