如果你已经对 JavaScript
很熟了, TypeScript
基本上也能快速上手,下面是我整理的一些初学者必备的一些知识点,如果你已经是个 TS
高手了,可以期待我后续的文章了~
Typescript 简介
据官方描述:TypeScript
是 JavaScript
的超集,这意味着它可以完成 JavaScript
所做的所有事情,而且额外附带了一些能力。
JavaScript
本身是一种动态类型语言,这意味着变量可以改变类型。使用 TypeScript
的主要原因是就是为了给 JavaScript
添加静态类型。静态类型意味着变量的类型在程序中的任何时候都不能改变。它可以防止很多bug !
Typescript 值得学吗?
下面是学习 Typescript
的几个理由:
- 研究表明,
TypeScript
可以发现15%
的常见bug
。 TypeScript
可以让代码的可读性更好,你可以更好的理解代码是在做什么。TypeScript
可以你申请到更多好工作。- 学习
TypeScript
可以使你对JavaScript
有更好的理解和新的视角。
当然,使用 Typescript
也有一些缺点:
TypeScript
的编写时间比JavaScript
要长,因为你必须要指定类型,对于一些较小的独立项目,可能不值使用。TypeScript
需要编译,项目越大消耗时间越长。
但是,相比于提前发现更多的 bug,花更长的时间也是值得的。
TypeScript 中的类型
原始类型
在 JavaScript
中,有 7 种原始类型:
string
number
bigint
boolean
undefined
null
symbol
原始类型都是不可变的,你可以为原始类型的变量重新分配一个新值,但不能像更改对象、数组和函数一样更改它的值。可以看下面的例子:
1 | let name = 'ConardLi'; |
回到 TypeScript
,我们可以在声明一个变量之后设置我们想要添加的类型 :type
(我们一般称之为“类型注释”或“类型签名”):
1 | let id: number = 5; |
但是,如果变量有默认值的话,一般我们也不需要显式声明类型,TypeScript
会自动推断变量的类型(类型推断):
1 | let id = 5; // number 类型 |
我们还可以将变量设置为联合类型(联合类型是可以分配多个类型的变量):
1 | let age: string | number; |
TypeScript 中的数组
在 TypeScript
中,你可以定义数组包含的数据类型:
1 | let ids: number[] = [1, 2, 3, 4, 5]; // 只能包含 number |
你也可以使用联合类型来定义包含多种类型的数组:
1 | let person: (string | number | boolean)[] = ['ConardLi', 1, true]; |
如果数组有默认值, TypeScript
同样也会进行类型推断:
1 | let person = ['ConardLi', 1, true]; // 和上面的例子一样 |
TypeScript
中可以定义一种特殊类型的数组:元组(Tuple
)。元组是具有固定大小和已知数据类型的数组,它比常规数组更严格。
1 | let person: [string, number, boolean] = ['ConardLi', 1, true]; |
TypeScript 中的对象
TypeScript
中的对象必须拥有所有正确的属性和值类型:
1 | // 使用特定的对象类型注释声明一个名为 person 的变量 |
在定义对象的类型时,我们通常会使用 interface
。如果我们需要检查多个对象是否具有相同的特定属性和值类型时,是很有用的:
1 | interface Person { |
我们还可以用函数的类型签名声明一个函数属性,通用函数(sayHi
)和箭头函数(sayBye
)都可以声明:
1 | interface Animal { |
需要注意的是,虽然 eat、speak
分别是用普通函数和箭头函数声明的,但是它们具体是什么样的函数类型都可以,Typescript
是不关心这些的。
TypeScript 中的函数
我们可以定义函数参数和返回值的类型:
1 | // 定义一个名为 circle 的函数,它接受一个类型为 number 的直径变量,并返回一个字符串 |
ES6 箭头函数的写法:
1 | const circle = (diam: number): string => { |
我们没必要明确声明 circle
是一个函数,TypeScript
会进行类型推断。TypeScript
还会推断函数的返回类型,但是如果函数体比较复杂,还是建议清晰的显式声明返回类型。
我们可以在参数后添加一个?,表示它为可选参数;另外参数的类型也可以是一个联合类型:
1 | const add = (a: number, b: number, c?: number | string) => { |
如果函数没有返回值,在 TS
里表示为返回 void
,你也不需要显式声明,TS
一样可以进行类型推断:
1 | const log = (msg: string): void => { |
any 类型
使 any
类型,我们基本上可以将 TypeScript
恢复为 JavaScript
:
1 | let name: any = 'ConardLi'; |
如果代码里使用了大量的 any
,那 TypeScript
也就失去了意义,所以我们应该尽量避免使用 any
。
DOM 和类型转换
TypeScript
没办法像 JavaScript
那样访问 DOM
。这意味着每当我们尝试访问 DOM
元素时,TypeScript
都无法确定它们是否真的存在。
1 | const link = document.querySelector('a'); |
使用非空断言运算符 (!
),我们可以明确地告诉编译器一个表达式的值不是 null
或 undefined
。当编译器无法准确地进行类型推断时,这可能很有用:
1 | // 我们明确告诉 TS a 标签肯定存在 |
这里我们没必要声明 link
变量的类型。这是因为 TypeScript
可以通过类型推断确认它的类型为 HTMLAnchorElement
。
但是如果我们需要通过 class
或 id
来选择一个 DOM
元素呢?这时 TypeScript
就没办法推断类型了:
1 | const form = document.getElementById('signup-form'); |
我们需要告诉 TypeScript
form
确定是存在的,并且我们知道它的类型是 HTMLFormElement
。我们可以通过类型转换来做到这一点:
1 | const form = document.getElementById('signup-form') as HTMLFormElement; |
TypeScript
还内置了一个 Event
对象。如果我们在表单中添加一个 submit
的事件侦听器,TypeScript
可以自动帮我们推断类型错误:
1 | const form = document.getElementById('signup-form') as HTMLFormElement; |
TypeScript 中的类
我们可以定义类中每条数据的类型:
1 | class Person { |
我们可以创建一个仅包含从 Person
构造的对象数组:
1 | let People: Person[] = [person1, person2]; |
我们可以给类的属性添加访问修饰符,TypeScript
还提供了一个新的 readonly
访问修饰符。
1 | class Person { |
我们可以通过下面的写法,属性会在构造函数中自动分配,我们类会更加简洁:
1 | class Person { |
如果我们省略访问修饰符,默认情况下属性都是
public
,另外和 JavaScript 一样,类也是可以extends
的。
TypeScript 中的接口
接口定义了对象的外观:
1 | interface Person { |
你还可以使用类型别名定义对象类型:
1 | type Person = { |
或者可以直接匿名定义对象类型:
1 | function sayHi(person: { name: string; age: number }) { |
interface
和 type
非常相似,很多情况下它俩可以随便用。比如它们两个都可以扩展:
扩展 interface
:
1 | interface Animal { |
扩展 type
:
1 | type Animal = { |
但是有个比较明显的区别,interface
是可以自动合并类型的,但是 type
不支持:
1 | interface Animal { |
类型别名在创建后无法更改:
1 | type Animal = { |
一般来说,当你不知道用啥的时候,默认就用 interface
就行,直到 interface
满足不了我们的需求的时候再用 type
。
类的 interface
我们可以通过实现一个接口来告诉一个类它必须包含某些属性和方法:
1 | interface HasFormatter { |
确保 people
是一个实现 HasFormatter
的对象数组(确保每 people
都有 format
方法):
1 | let people: HasFormatter[] = []; |
泛型
泛型可以让我们创建一个可以在多种类型上工作的组件,它能够支持当前的数据类型,同时也能支持未来的数据类型,这大大提升了组件的可重用性。我们来看下面这个例子:
addID
函数接受一个任意对象,并返回一个新对象,其中包含传入对象的所有属性和值,以及一个 0
到 1000
之间随机的 id
属性。
1 | const addID = (obj: object) => { |
当我们尝试访问 name
属性时,TypeScript
会出错。这是因为当我们将一个对象传递给 addID
时,我们并没有指定这个对象应该有什么属性 —— 所以 TypeScript
不知道这个对象有什么属性。因此,TypeScript
知道的唯一属性返回对象的 id
。
那么,我们怎么将任意对象传递给 addID
,而且仍然可以告诉 TypeScript
该对象具有哪些属性和值?这种场景就可以使用泛型了, <T>
– T
被称为类型参数:
1 | // <T> 只是一种编写习惯 - 我们也可以用 <X> 或 <A> |
这是啥意思呢?现在当我们再将一个对象传递给 addID
时,我们已经告诉 TypeScript
来捕获它的类型了 —— 所以 T
就变成了我们传入的任何类型。addID
现在会知道我们传入的对象上有哪些属性。
但是,现在有另一个问题:任何东西都可以传入 addID
,TypeScript
将捕获类型而且并不会报告问题:
1 | let person1 = addID({ name: 'ConardLi', age: 17 }); |
当我们传入一个字符串时,TypeScript
没有发现任何问题。只有我们尝试访问 name
属性时才会报告错误。所以,我们需要一个约束:我们需要通过将泛型类型 T
作为 object
的扩展,来告诉 TypeScript
只能接受对象:
1 | const addID = <T extends object>(obj: T) => { |
错误马上就被捕获了,完美…… 好吧,也不完全是。在 JavaScript
中,数组也是对象,所以我们仍然可以通过传入数组来逃避类型检查:
1 | let person2 = addID(['ConardLi', 17]); // 传递数组没问题 |
要解决这个问题,我们可以这样说: object
参数应该有一个带有字符串值的 name
属性:
1 | const addID = <T extends { name: string }>(obj: T) => { |
泛型允许在参数和返回类型提前未知的组件中具有类型安全。
在 TypeScript
中,泛型用于描述两个值之间的对应关系。在上面的例子中,返回类型与输入类型有关。我们用一个泛型来描述对应关系。
另一个例子:如果需要接受多个类型的函数,最好使用泛型而不是 any 。下面展示了使用 any
的问题:
1 | function logLength(a: any) { |
我们可以尝试使用泛型:
1 | function logLength<T>(a: T) { |
好,至少我们现在得到了一些反馈,可以帮助我们持续改进我们的代码。
解决方案:使用一个泛型来扩展一个接口,确保传入的每个参数都有一个 length
属性:
1 | interface hasLength { |
我们也可以编写这样一个函数,它的参数是一个元素数组,这些元素都有一个 length
属性:
1 | interface hasLength { |
泛型是 TypeScript
的一个很棒的特性!
泛型接口
当我们不知道对象中的某个值是什么类型时,可以使用泛型来传递该类型:
1 | // The type, T, will be passed in |
枚举
枚举是 TypeScript
给 JavaScript
带来的一个特殊特性。枚举允许我们定义或声明一组相关值,可以是数字或字符串,作为一组命名常量。
1 | enum ResourceType { |
默认情况下,枚举是基于数字的 — 它们将字符串值存储为数字。但它们也可以是字符串:
1 | enum Direction { |
当我们有一组相关的常量时,枚举就可以派上用场了。例如,与在代码中使用非描述性数字不同,枚举通过描述性常量使代码更具可读性。
枚举还可以防止错误,因为当你输入枚举的名称时,智能提示将弹出可能选择的选项列表。
TypeScript 严格模式
建议在 tsconfig.json
中启用所有严格的类型检查操作文件。这可能会导致 TypeScript
报告更多的错误,但也更有助于帮你提前发现发现程序中更多的 bug
。
1 | // tsconfig.json |
严格模式实际上就意味着:禁止隐式 any 和 严格的空检查。
禁止隐式 any
在下面的函数中,TypeScript
已经推断出参数 a
是 any
类型的。当我们向该函数传递一个数字,并尝试打印一个 name
属性时,没有报错:
1 | function logName(a) { |
打开 noImplicitAny
选项后,如果我们没有显式地声明 a
的类型,TypeScript
将立即标记一个错误:
1 | // ERROR: Parameter 'a' implicitly has an 'any' type. |
严格的空检查
当 strictNullChecks
选项为 false
时,TypeScript
实际上会忽略 null
和 undefined
。这可能会在运行时导致意外错误。
当 strictNullChecks
设置为 true
时,null
和 undefined
有它们自己的类型,如果你将它们分配给一个期望具体值(例如,字符串)的变量,则会得到一个类型错误。
1 | let whoSangThis: string = getSong(); |
singles.find
并不能保证它一定能找到这首歌 — 但是我们已经编写了下面的代码,好像它肯定能找到一样。
通过将 strictNullChecks
设置为 true
, TypeScript
将抛出一个错误,因为在尝试使用它之前,我们没有保证 single
一定存在:
1 | const getSong = () => { |
TypeScript
基本上是告诉我们在使用 single
之前要确保它存在。我们需要先检查它是否为 null
或 undefined
:
1 | if (single) { |
TypeScript 中的类型收窄
在 TypeScript
中,变量可以从不太精确的类型转移到更精确的类型,这个过程称为类型收窄。
下面是一个简单的例子,展示了当我们使用带有 typeof
的 if
语句时,TypeScript
如何将不太特定的 string | number
缩小到更特定的类型:
1 | function addAnother(val: string | number) { |
另一个例子:下面,我们定义了一个名为 allVehicles
的联合类型,它可以是 Plane
或 Train
类型。
1 | interface Vehicle { |
由于 getSpeedRatio
函数处理了多种类型,我们需要一种方法来区分 v
是 Plane
还是 Train
。我们可以通过给这两种类型一个共同的区别属性来做到这一点,它带有一个字符串值:
1 | interface Train extends Vehicle { |
现在,TypeScript
可以缩小 v 的类型:
1 | function getSpeedRatio(v: PlaneOrTrain) { |
另外,我们还可以通过实现一个类型保护来解决这个问题,可以看看这篇文章:什么是鸭子🦆类型?
TypeScript & React
TypeScript 完全支持 React 和 JSX。这意味着我们可以将 TypeScript 与三个最常见的 React 框架一起使用:
create-react-app
(https://create-react-app.dev/docs/adding-typescript/)Gatsby
(https://www.gatsbyjs.com/docs/how-to/custom-configuration/typescript/)Next.js
(https://nextjs.org/learn/excel/typescript)
如果你需要一个更自定义的 React-TypeScript
配置,你可以字节配置 Webpack
和 tsconfig.json
。但是大多数情况下,一个框架就可以完成这项工作。
例如,要用 TypeScript
设置 create-react-app
,只需运行:
1 | npx create-react-app my-app --template typescript |
在 src
文件夹中,我们现在可以创建带有 .ts
(普通 TypeScript
文件)或 .tsx
(带有 React
的 TypeScript
文件)扩展名的文件,并使用 TypeScript
编写我们的组件。然后将其编译成 public
文件夹中的 JavaScript
。
React props & TypeScript
Person
是一个 React
组件,它接受一个 props
对象,其中 name
应该是一个字符串,age
是一个数字。
1 | // src/components/Person.tsx |
一般我们更喜欢用 interface
定义 props
:
1 | interface Props { |
然后我们尝试将组件导入到 App.tsx
,如果我们没有提供必要的 props
,TypeScript
会报错。
1 | import React from 'react'; |
React hooks & TypeScript
useState()
我们可以用尖括号来声明状态变量的类型。如果我们省略了尖括号,TypeScript
会默认推断 cash
是一个数字。因此,如果想让它也为空,我们必须指定:
1 | const Person: React.FC<Props> = ({ name, age }) => { |
useRef()
useRef
返回一个可变对象,该对象在组件的生命周期内都是持久的。我们可以告诉 TypeScript
ref
对象应该指向什么:
1 | const Person: React.FC = () => { |
参考
- https://www.typescriptlang.org/docs/
- https://react-typescript-cheatsheet.netlify.app/
- https://www.freecodecamp.org/news/learn-typescript-beginners-guide
如果你想加入高质量前端交流群,或者你有任何其他事情想和我交流也可以添加我的个人微信 ConardLi 。