如何不学习TypeScript
发表于2022年1月10日
作者:@ddprrt
阅读时间:15分钟
更多关于TypeScript的内容
"TypeScript和我永远不会成为朋友"。哦,哇,我有多少次听到这句话了?学习TypeScript,即使是在2022年,似乎也是令人沮丧的。而且有很多不同的原因。写Java或C#的人,发现事情的工作方式与他们应该的不同。那些大部分时间都在做JavaScript的人,正在被编译器尖叫着。以下是我看到的人们在开始使用TypeScript时犯的一些错误。我希望它们对你有帮助!
这篇文章在很大程度上受到了Denys的《如何不学习Rust》的影响,我可以强烈推荐。
错误1:忽略JavaScript#
TypeScript是JavaScript的超集,并且一直以来都是这样宣传的。这意味着,JavaScript在很大程度上是语言的一部分。它的全部。选择TypeScript并不能给你一张免费的卡片来抛弃JavaScript和它的不稳定行为。但TypeScript让你更容易理解它。而且你可以看到JavaScript到处都有突破。
例如,请看我关于错误处理的博文。如果像你在其他编程语言中习惯的那样,允许捕捉错误,那是非常合理的。
try { // something with Axios, for example} catch(e: AxiosError) {// ^^^^^^^^^^ Error 1196 💥}
但这是不可能的。原因是JavaScript的错误是如何工作的(查看相关文章以了解更多细节)。这些代码在TypeScript中是有意义的,但在JavaScript中是做不到的。
另一个例子,使用Object.keys ,期待简单的属性访问,这也是你所期待的,但会造成问题。
type Person = { name: string, age: number, id: number,}declare const me: Person;Object.keys(me).forEach(key => { // 💥 the next line throws red squigglies at us console.log(me[key])})
有一种方法可以修补这种行为,正如这里所详述的,但这个补丁不能适用于每一种情况。TypeScript不能根据你的代码来保证这种属性访问的类型是你所期望的。单独在JavaScript中运行完全正常的代码,但由于很多原因,很难用类型系统来表达。
如果你在学习TypeScript时没有任何JavaScript背景,那么就开始学习区分JavaScript和类型系统。同时,要学会搜索正确的东西。函数中的命名参数。你可以用对象作为参数来做。一个不错的模式。不过,这是JavaScript的一部分。条件链?首先在TypeScript编译器中实现,但它也是一个JavaScript特性。类和扩展现有的类?JavaScript。私有类字段?你知道,那些前面有# ,一个小栅栏,所以没有人可以访问它后面的东西。也是JavaScript。
真正做事情的程序代码在大多数情况下是属于JavaScript阵营的。如果你使用类型来表达意图和契约,你就在类型的土地上。
最近,TypeScript网站对使用TypeScript的含义有了更清晰的表述。TypeScript是带有类型语法的JavaScript。它就在这里。TypeScript就是JavaScript。理解JavaScript是理解TypeScript的关键。
错误2:给所有东西加注#
类型注解是一种明确告诉人们期待哪些类型的方式。你知道,那些在其他编程语言中非常突出的东西,StringBuilder stringBuilder = new StringBuilder() ,确保你真的,真的在处理一个StringBuilder 。相反的是类型推理,TypeScript试图为你找出类型。let a_number = 2 ,属于number 。
类型注释也是TypeScript和JavaScript之间最明显和可见的语法差异。
当你开始学习TypeScript的时候,你可能想要注释所有的东西来表达你所期望的类型。在开始学习TypeScript时,这可能是一个显而易见的选择,但我恳请你少用注释,让TypeScript为你找出类型。为什么呢?让我解释一下类型注解到底是什么。
类型注解是一种让你表达合约必须被检查的方式。如果你在变量声明中添加类型注解,你就会告诉编译器在赋值时检查类型是否匹配。
type Person = { name: string, age: number}const me: Person = createPerson()
如果createPerson 返回的东西与Person 不兼容,TypeScript将出错。如果你真的想确定你在这里处理的是正确的类型,那就这么做吧。
另外,从那一刻起,me 是类型Person ,TypeScript将把它当作一个Person 。如果在me 中有更多的属性,例如一个profession ,TypeScript将不允许你访问它们。它没有定义在Person 。
如果你在一个函数签名的返回值上添加一个类型注解,你告诉编译器在你返回这个值的时候要检查类型是否匹配。
function createPerson(): Person { return { name: "Stefan", age: 39 }}
如果我返回的东西不符合Person ,TypeScript会出错。如果你想完全确定你返回的是正确的类型,请这样做。如果你正在处理从不同来源构造大对象的函数,这一点尤其有用。
如果你在函数签名的参数上添加类型注解,你会告诉编译器在你传递参数的时候检查类型是否匹配。
function printPerson(person: Person) { console.log(person.name, person.age)}printPerson(me)
在我看来,这是最重要的,也是不可避免的类型注解。其他的都可以被推断出来。
type Person = { name: string, age: number}// Inferred!// return type is { name: string, age: number }function createPerson() { return { name: "Stefan", age: 39}}// Inferred!// me is type of { name: string, age: number}const me = createPerson() // Annotated! You have to check if types are compatiblefunction printPerson(person: Person) { console.log(person.name, person.age)}// All worksprintPerson(me)
总是对函数参数使用类型注解。这是你必须要检查你的合同的地方。这不仅更方便,而且还带来了大量的好处。例如,你可以免费获得多态性。
type Person = { name: string, age: number}type Studying = { semester: number}type Student = { id: string, age: number, semester: number}function createPerson() { return { name: "Stefan", age: 39, semester: 25, id: "XPA"}}function printPerson(person: Person) { console.log(person.name, person.age)}function studyForAnotherSemester(student: Studying) { student.semester++}function isLongTimeStudent(student: Student) { return student.age - student.semester / 2 > 30 && student.semester > 20}const me = createPerson() // All work!printPerson(me)studyForAnotherSemester(me)isLongTimeStudent(me)
Student Person 和 有一些重叠,但彼此不相关。 返回与所有三种类型兼容的东西。如果我们注释的太多,我们就需要创建更多的类型和更多的检查,而没有任何好处。Studying createPerson
在学习TypeScript时,不要过多地依赖类型注释,这也会让你真正感受到使用结构类型系统意味着什么。
错误3:错把类型当做值#
TypeScript是JavaScript的一个超级集合,这意味着它为已经存在和定义的语言增加了更多的东西。随着时间的推移,你要学会识别哪些部分是JavaScript,哪些部分是TypeScript。
把TypeScript看成是普通JavaScript上的额外类型层,这真的很有帮助。一层薄薄的元信息,在你的JavaScript代码在一个可用的运行时中运行之前,它将被剥去。有些人甚至说TypeScript的代码一旦编译就会 "清除成JavaScript"。
TypeScript作为JavaScript之上的这一层,也意味着不同的语法会对不同的层做出贡献。当一个function 或const 在JavaScript部分创建一个名字时,一个type 声明或一个interface 在TypeScript层贡献一个名字。比如说。
// Collection is in TypeScript land! --> typetype Collection<T> = { entries: T[]}// printCollection is in JavaScript land! --> valuefunction printCollection(coll: Collection<unknown>) { console.log(...coll.entries)}
我们还说,名字或声明贡献了一个类型或一个值。因为类型层在值层之上,所以在类型层中消耗值是可能的,但反之就不行。我们也有明确的关键字用于此。
// a valueconst person = { name: "Stefan"}// a typetype Person = typeof person;
typeof 从下面的值层中创建一个在类型层中可用的名字。
当有同时创建类型和值的声明类型时,它就会变得很恼火。例如,类可以在TypeScript层中作为类型使用,也可以在JavaScript中作为值使用。
// declarationclass Person { name: string constructor(n: string) { this.name = n }}// valueconst person = new Person("Stefan")// typetype PersonCollection = Collection<Person>function printPersons(coll: PersonCollection) { //...}
而且命名惯例也会欺骗你。通常情况下,我们定义类、类型、接口、枚举等都是用大写的第一个字母。而且,即使它们可能贡献值,它们也肯定贡献类型。好吧,至少在你为你的React应用写大写的函数之前。
如果你习惯于使用名字作为类型和值,如果你突然得到一个好的老TS2749:'YourType'指的是一个值,但被用作一个类型的错误,你会挠头。
type PersonProps = { name: string}function Person({ name }: PersonProps) { return <p>{name}</p>}type Collection<T> = { entries: T}type PrintComponentProps = { collection: Collection<Person> // ERROR! // 'Person' refers to a value, but is being used as a type}
这就是TypeScript可能变得非常混乱的地方。什么是类型,什么是值,为什么我们需要分离这些,为什么不像其他编程语言那样工作?突然间,你看到自己面临着typeof ,甚至是InstanceType 辅助类型,因为你意识到类实际上贡献了两种类型(令人震惊!)。
所以,了解什么贡献了类型,什么贡献了价值是很好的。边界是什么,我们可以如何以及向哪个方向移动,这对你的类型意味着什么?这个表格,改编自TypeScript的文档,很好地总结了这一点。
| 声明类型 | 类型 | 价值 |
|---|---|---|
| 类型 | X | X |
| 枚举 | X | X |
| 介面 | X | |
| 类型别名 | X | |
| 功能 | X | |
| 变量 | X |
在学习TypeScript时,关注函数、变量和简单的类型别名(或接口,如果这更适合你的话)可能是个好主意。这应该会让你对类型层发生的事情和价值层发生的事情有一个很好的概念。
错误4:一开始就全身心投入#
我们已经说了很多关于某人从不同的编程语言来到TypeScript会犯什么错误。公平地说,这已经是我在相当长一段时间内的面包和黄油了。但也有一个不同的轨迹。那些已经写了很多JavaScript的人,突然面对另一个有时非常恼人的工具。
这可能会导致非常令人沮丧的经历。你对你的代码库了如指掌,突然一个编译器告诉你,它不理解左右的东西,你犯了错误,尽管你知道你的软件会工作。
你想知道为什么每个人都会喜欢这个家伙。TypeScript应该帮助你提高工作效率,但它所做的只是在你的代码下抛出令人分心的红色斜线。
我们都经历过这种情况,不是吗?
我也能体会到这一点!TypeScript可能非常吵闹,尤其是当你在现有的JavaScript代码库中 "刚刚打开它 "时。TypeScript想要了解你的整个应用程序,这就要求你对所有的东西进行注释,以便合同对齐。这真是太麻烦了。
如果你来自JavaScript,我会说你应该利用TypeScript的渐进式采用功能。TypeScript的设计是为了让你在全身心投入之前,尽可能轻松地采用一点儿。
- 将你的应用程序的一部分转移到TypeScript,而不是全部转移。TypeScript 具有 JavaScript 互操作性 (
allowJS) - 即使TypeScript在你的代码中发现了错误,TypeScript也会发射编译的JavaScript代码。你必须使用
noEmitOnError标志明确关闭代码发射。这允许你即使在你的编译器尖叫的时候仍然可以发货 - 通过编写类型声明文件并通过JSDoc导入它们来使用TypeScript。这是一个很好的第一步,可以获得更多关于你的代码库内发生的信息。
- 在任何地方使用都会让人难以承受或太过费力。与流行的观点相反,使用any是绝对可以的,只要它是明确使用的。
查看tsconfig 参考,看看哪些配置标志是可用的。TypeScript是为逐步采用而设计的。你可以随心所欲地使用许多类型。你可以把你的应用程序的很大一部分留在JavaScript中,这绝对应该帮助你开始。
当作为一个JavaScript开发者学习TypeScript时,不要对自己要求太高。试着把它作为内联文档,以更好地推理你的代码,并在此基础上进行扩展/改进。
错误5:学习错误的TypeScript#。
再次,从如何不学习Rust中得到了很大启发。如果你的代码需要使用以下关键词之一,那么你可能是在TypeScript的错误角落里,或者比你想要的要远得多。
namespacedeclaremodule<reference>abstractunique
这并不意味着这些关键词没有贡献非常重要的东西,而且对于各种使用情况来说都是必要的。在学习TypeScript时,你不会想在一开始就使用它们。
就这样吧!我很好奇你是如何学习TypeScript的,以及你在开始时遇到了什么障碍。另外,你还知道其他可能是学习TypeScript时的常见错误的事情吗?让我知道吧!我很想听听你的故事。
我已经写了一本关于TypeScript的书!查看《TypeScript的50堂课》,由Smashing杂志出版。
TL/DR🏃♀️🚶♀️庆祝🎉🎉