用大白话讲 TypeScript,两小时快速上手TypeScript (上)
start
-
最近接触到一些前辈写的
TypeScript
代码,我发现,我明明非常熟悉javascript
,但是看TypeScript
就像看天书一样,一头雾水。 -
每次下定决心说要去深入学习
TypeScript
,但是到最后却被巨多的文字教程所劝退。 -
我仔细分析了一下我一直没掌握
TypeScript
的原因,无外乎两点:- 分不清主次; (官方教程肯定是对这个知识点尽可能全面详细的说明,但是对新手来说,入门的时候,太多细节只会让人麻木)
- 缺乏使用场景;(和我们大脑记忆方式有关,再好的东西,长时间不使用自然会遗忘)
-
所以我打算写一个,只讲
TypeScript
常用知识的博客 ,博客目标:学习完之后能看懂别人写的ts
代码。先入门了,再看全面的教程优化细节。-
只讲常用内容
-
编写博客,加深记忆。
- 这就是我思考出的破局方法。而且我在开始编写博客的时候,并没完全掌握
TypeScript
,抱着新手的视角去编写博客,应该会更加适合新手入门。祝好。 - 能力有限,所有个人理解,可能有误,仅供参考,
-
-
后续为了方便编写本文
TypeScript
统一简称为ts
。 -
本文作者: lazy_tomato
-
编写时间:2024-03-06
1. 什么是 TypeScript ?
TypeScript官网解释:TypeScript is JavaScript with syntax for types.
英译:
TypeScript
是带有类型语法的JavaScript
。
简单来说,ts
就是在 js
的基础上扩展了类型语法,有了类型语法,有利于代码的静态分析;发现错误;做到语法提示和自动补全;
举个例子,你封装了一个函数 add
,有两个参数 a
和 b
,函数的作用是返回 a
和 b
的和 。由于涉及到加法,所以你希望传入的两个参数都是数字类型的,不希望有其他类型的,此时就可以使用 ts
做类型限制。
function add(a: number, b: number) {
return a + b
}
add(1, 2)
add('123', '123') // 类型“string”的参数不能赋给类型“number”的参数。
还有一个要注意的,ts
不能直接执行,需要借助官方插件 tsx
,将 ts
编译成 js
再去执行。
- 不过我们实际编写
ts
的时候,并没有这么麻烦。- 大多数的时候,比如
react
,vue
,Cocos creater 3.x
内部已经集成好了tsx
编译环境,可以让我们直接去写ts
即可,不需要我们再额外手动处理tsx
。- 针对高定制化的需求,我们也可以修改对应的配置文件, 定义编译选项。(以具体的使用环境为准)
2. 如何声明类型?
如何声明类型,简单来说:使用冒号,声明类型。
// 1. 给变量声明类型-- 下方foo为字符串类型
let foo:string;
// 2. 给函数的形参num 声明类型 number
// 3. 给toString函数的返回值定义类型 string
function toString(num:number):string {
return String(num);
}
这个时候要培养一个感觉,要熟悉 ts
的写法,可能一眼看去代码量很多。但是,大多数时候,冒号后面内容无论写的再长在复杂。我们在理解代码逻辑的时候,如果因此受到困扰,可以选择先忽略这些复杂的类型。
我们再来体验一下这种感觉。
export function startMeasure(
instance: ComponentInternalInstance,
type: string
) {}
export function emit(
instance: ComponentInternalInstance,
event: string,
...rawArgs: any[]
) {}
不看类型限制,上方的函数,其实就是下方的函数,其实就是一个很普通的函数,仅此而已。
export function startMeasure(instance, type) {}
export function emit(instance, event, ...rawArgs) {}
ok,本节就讲这么多,有这种感觉后,至少能保证就算类型定义的再复杂,我们能看得懂基础的代码。
3. 可以声明的类型有哪些?
3.1 基本类型
上一节我们熟悉了如何声明类型,现在有一个问题:有哪些类型可以声明呢?
从上面的知识我们可以了解到, ts
是在 js
基础上扩展了类型语法,结合第 2 节的示例代码,我们大胆猜测一下。既然 js
中的自带 8 种数据类型,那么 ts
应当支持这 8 种类型吧?
答案:TypeScript
继承了 JavaScript 的类型设计,以下 8 种类型可以看作 TypeScript
的基本类型。
- boolean
- string
- number
- bigint
- symbol
- object
- undefined
- null
这 8 种类型,也不用死记硬背,对标 js
的 8 种数据类型即可。
要注意一下,类型都是用小写字母。首字母大写的
Number
、String
、Boolean
等在 JavaScript 语言中都是内置对象,而不是类型名称。
想看更详细的介绍: TypeScript 教程–阮一峰–基本类型
3.2 基于基本类型的拓展
基于基本类型的拓展(为了方便记忆,我自己定义的一个归类。)
上一节内容说到了 8 种基本类型,但是在实际使用的时候,面对不同的声明类型需求,我们需要一些更加灵活的方式。
比如:
- 我希望变量只能是某一个值;
- 我希望变量可以是多种类型中的一个;
- 我希望变量可以同时满足多种类型;
分别对应:
值类型
TypeScript
规定,单个值也是一种类型,称为“值类型”。
联合类型
联合类型(union types)指的是多个类型组成的一个新类型,使用符号
|
表示。
交叉类型
交叉类型(intersection types)指的多个类型组成的一个新类型,使用符号
&
表示。
1. 值类型
/* 1. 值类型 */
let flag: 5
flag = 5
// flag = 10 // 不能将类型“10”分配给类型“5”。
2. 联合类型
/* 2. 联合类型 */
// id 既可以是数字也可以是字符串
function printId(id: number | string) {
if (typeof id === 'string') {
console.log(id.toUpperCase())
} else {
console.log(id)
}
}
3. 交叉类型
/* 3. 交叉类型 */
// 既有 字符串的foo,又有 字符串的bar
let obj: { foo: string } & { bar: string }
obj = {
foo: 'hello',
bar: 'world',
}
// 交叉类型常用来为对象类型添加新属性
个人理解
上述介绍的几个类型,其实就是对基础类型的简易组合:
-
把一个固定的值当做类型;
-
把基本类型,做
| 或
或者& 与
逻辑,做一个联合或者交叉。
3.3 any 类型
为了方便代码从 js
迁移到 ts
,降低学习难度,在 ts
中直接编写 js
代码,不添加任何类型限制是完全兼容的。
那如果什么类型都没有提供,ts
会如何记录变量的类型呢?
借助 vscode 的代码编辑器的提示,我们可以看到,提示变量的类型为 any
。
function add(a, b) {
return a + b
}
3.3.1 any (任何)
any
类型表示没有任何限制,该类型的变量可以赋予任意类型的值。
相当于,你随便给这个变量传什么值都可以,和
js
的语法很像;
为什么编辑器会自动提示 any
类型呢?我并没有给示例代码中的 add
,a
,b
显式定义 any
。
原因:对于开发者没有指定类型、
TypeScript
必须自己推断类型的那些变量,如果无法推断出类型,TypeScript
就会认为该变量的类型是any
。简单来说,
ts
会自己推断变量的类型,如果无法推断出来,则认为该变量是any
。
使用 any
类型的变量可以赋予任意值,但是与之而来的就是两个缺点:
-
主动放弃了对这个变量的类型限制。
-
我们使用
ts
的目的就是增加类型限制,设置类型为any
,丢失了使用ts
的意义。 -
当然某些时候:变量的类型我们刚接触并不清楚,为了快速完成功能,适当的
any
也是可以的.- 结论: 可以用,但是不建议。
-
-
类型污染
-
any
会“污染”其他变量。它可以赋值给其他任何类型的变量(因为没有类型检查),导致其他变量出错。let x:any = 'hello'; let y:number; y = x; // 不报错 y * 123 // 不报错 y.toFixed() // 不报错
-
3.3.2 unknown (未知的)
为了解决 any
类型“污染”其他变量的问题,TypeScript 3.0
引入了 unknown
类型。
unknown
类型特点:
unknown
可以赋值为各种类型的值。- 不能直接赋值给其他类型(其他类型 除了
any
类型和unknown
类型)的变量。 - 不能直接调用
unknown
类型变量的方法和属性。 unknown
类型变量能够进行的运算是有限的,只能进行比较运算(运算符==
、===
、!=
、!==
、||
、&&
、?
)、取反运算(运算符!
)、typeof
运算符和instanceof
运算符这几种,其他运算都会报错。
如何才能使用 unknown
类型变量呢?
let a:unknown = 1;
// 类型缩小
if (typeof a === 'number') {
let r = a + 10; // 正确
}
个人理解
为了解决 any
的类型污染,引入 unknown
类型,可以接收任何值,不过在使用的时候,需要增加额外的判断才能使用。达到解决类型污染的问题。
简单理解就是,unknown
被限制使用的 any
。
3.3.3 never (从来没有)
为了保持与集合论的对应关系,以及类型运算的完整性,TypeScript 还引入了“空类型”的概念,即该类型为空,不包含任何值。
由于不存在任何属于“空类型”的值,所以该类型被称为never
,即不可能有这样的值。
例如:
// 一个变量又是数字类型又是字符串类型,显然不可能,此时a就是never类型
let a: number & string
个人理解
其实就是为了保证类型的完整性,每个变量都有类型。针对那种不可能有值的情况,定义这个变量为 never
。
4. type 和 interface
我这篇文章的目的就是希望学习完毕后,能够看懂别人的 ts
代码。
我们目前已经学习了一部分内容,现在找一个 ts
代码验证一下。我打开 vue3
的源代码仓库,看看他们写的代码,看我们是否能看懂。
vue3 中的一个 ts 文件截图:
按常理,js
文件中最顶部的代码,就是引入一些关联模块。结合上图,我发现除了引入了一些关联模块,还有两个单词高频出现:type,interface,它们是什么意思? 我们去学习一下。
英译:
type:类型
interface: 接口
4.1 type
4.1.1 type 作用
type
命令用来 定义一个类型的别名。
/* 1. type 用于定义类型的别名 */
// 案例一
type Age = number
let age: Age = 55
// 案例二
type All = number | string
let like: All = 777
like = '爱吃番茄'
4.1.2 type 注意事项
- 别名不能重名;
- 别名的作用域是块级作用域;
别名不能重名
别名的作用域是块级作用域
4.1.3 个人理解
type
简单来说,可以定义类型的名称,就好像我们定义一个变量存储数据一样。不过 type
定义的是类型名,存储的是类型数据。
请切记:不要被复杂的名称劝退。
比如:
type TLaztTomatoLikeJavaScript = number
type ___sdmpwqmem_sawqen12321_qwmeqwmmwo = number
type SFCStyleCompileOptions = number
// 其实上面的变量,只是看起来复杂而已,它们都是代表着是一个 数字 类型。
4.2 interface
4.2.1 声明对象的类型
说 interface
之前,说说我自己目前的疑惑。前面提到,大多数情况下定义类型呢,我们使用 : 冒号
。
比如:
function toString(num:number):string {
return String(num);
}
我有什么疑惑呢? 目前学习到的知识,声明类型:直接在变量后面加冒号 变量 :类型
,那么如何声明对象的属性的类型呢?
对象中的属性名后面的冒号,代表着给这个属性赋值,那么该如何用冒号声明类型呢?
let a: string
let obj = {
name: 'lazytomato', // name 后面的冒号代表着给这个属性去赋值,肯定是不可以在 name 后面直接加冒号定义类型的。
}
我们可以直接给 obj 后加冒号,声明对象的类型,例如:
let obj: {
name: string
} = {
name: 'lazyTomato',
}
obj = {
name: 123, // 赋值 number 类型, 会报错
}
简单介绍下,定义对象的类型的其他情况:
let obj: {
a: string // 1. 必须包含的属性
b?: string // 2. 可选的属性
readonly c: string // 3. 必须包含的属性,且只读
} = {
a: 'lazyTomato',
c: '123',
}
// obj.c = '爱吃番茄' // 无法为“c”赋值,因为它是只读属性。
上面的示例介绍了,对象中:
- 普通的属性
- 可选的属性
- 只读的属性
4.2.2 简化对象的类型声明(type interface 初体验)
前一节内容,可以看到给对象的属性声明类型。但是每次都写一大串内容,肯定是不够方便的。
这个时候就需要简写形式。
有两种简化方式:
type
定义别名;- 定义一个
interface
接口
// 方法一 type定义别名
type myObj = {
a: string
b?: string
readonly c: string
}
let obj: myObj = {
a: 'lazyTomato',
c: '123',
}
// 方法二 定义一个接口
interface myObj2 {
a: string
b?: string
readonly c: string
}
let obj2: myObj2 = {
a: 'lazyTomato',
c: '123',
}
其实大家可以自己用手敲一下上面两种方式,再看下面的内容。
我讲一讲我的切实感受:
type
是定义别名,所以需要有一个=
。interface
很像是定义一个class
形式的对象,不需要=
。
type
是定义别名,所以可以定义:基础类型
,以及基础类型的一些拓展 (值,联合,交叉类型)
。interface
是定义对象,不能直接设置基础类型。type a = number type b = 5 type c = number | (string & boolean) type d = { name: string } interface e { name: string } interface f number // 这里会报错
4.2.3 interface 是什么
interface
介绍:interface
是对象的模板,可以看作是一种类型约定,中文译为“接口”。使用了某个模板的对象,就拥有了指定的类型结构。
个人理解:以对象形式去声明类型的形式。
再大白话一点,就是定义了一个对象,不过这个对象存储的是
对象属性名
和对应属性值的类型
。
想看更详细的介绍: TypeScript 教程–阮一峰–interface
4.2.4 interface 写法介绍
interface 可以表示对象的各种语法,它的成员有5种形式。
- 对象属性
- 对象的属性索引
- 对象方法
- 函数
- 构造函数
1. 对象属性
interface Point {
x: number;
y: number;
}
2. 对象的属性索引
interface A {
[prop: string]: number;
}
// 定义 属性名是字符串类型, 属性值是数字
3. 对象的方法
// 写法一
interface A {
f(x: boolean): string;
}
// 写法二
interface B {
f: (x: boolean) => string;
}
// 写法三
interface C {
f: { (x: boolean): string };
}
4. 函数
interface Add {
(x:number, y:number): number;
}
5. 构造函数
interface ErrorConstructor {
new (message?: string): Error;
}
// interface 内部可以使用`new`关键字,表示构造函数。
4.2.5 interface 注意事项
-
interface
可以继承。先混个眼熟,知道可以用
extends
继承接口即可,整体形式有点类似对象的操作。 -
多个
interface
会自动合并interface a { name: string } interface a { // name: number // 前面声明过了 name,后面再声明的时候,会报错,提示必须和前一次类型相同。 age: number } let obj: a = { name: 'tomato', age: 18, }
-
块级作用域
interface a { name: string } if (true) { interface a { age: number } } let obj: a = { name: 'tomato', age: 18, // 报错 对象字面量只能指定已知属性,并且“age”不在类型“a”中 }
4.3 type 和 interface 异同
相同点:
interface
命令与type
命令作用类似,都可以表示对象类型。
不同点:
-
type
能够表示非对象类型,而interface
只能表示对象类型(包括数组、函数等)。 -
interface
可以继承其他类型,type
不支持继承。 -
同名
interface
会自动合并,同名type
则会报错。 -
interface
不能包含属性映射(mapping),type
可以。 -
this
关键字只能用于interface
-
type
可以扩展原始数据类型,interface
不行。
个人理解
列了一堆内容,其实很简单,两者都可以简化类型的声明。不过两者的侧重点不同。
-
type
更倾向于描述:一个组合的类型,简化成一个名称,同一作用域下保证唯一。 -
interface
更倾向于描述:对象形式的类型,支持继承,this
,易扩展。
推荐:定义对象的类型,就用
interface
,interface
做不到的用type
。
4.4 小试牛刀
刚学完 type
和 interface
,我们再看看部分 Vue3
的 ts
源码,看能不能看懂。
interface
export interface SFCStyleCompileOptions {
source: string
filename: string
id: string
scoped?: boolean
trim?: boolean
isProd?: boolean
inMap?: RawSourceMap
preprocessLang?: PreprocessLang
preprocessOptions?: any
preprocessCustomRequire?: (id: string) => any
postcssOptions?: any
postcssPlugins?: any[]
/**
* @deprecated use `inMap` instead.
*/
map?: RawSourceMap
}
export interface CSSModulesOptions {
scopeBehaviour?: 'global' | 'local'
generateScopedName?: | string
| ((name: string, filename: string, css: string) => string)
hashPrefix?: string
localsConvention?: 'camelCase' | 'camelCaseOnly' | 'dashes' | 'dashesOnly'
exportGlobals?: boolean
globalModulePaths?: RegExp[]
}
export interface SFCStyleCompileResults {
code: string
map: RawSourceMap | undefined
rawResult: Result | LazyResult | undefined
errors: Error[]
modules?: Record<string, string>
dependencies: Set<string>
}
type
export type ParentNode = RootNode | ElementNode | IfBranchNode | ForNode
export type ExpressionNode = SimpleExpressionNode | CompoundExpressionNode
export type TemplateChildNode =
| ElementNode
| InterpolationNode
| CompoundExpressionNode
| TextNode
| CommentNode
| IfNode
| IfBranchNode
| ForNode
| TextCallNode
个人理解
- 第一,学习了
type
和interface
,再看上面的代码,感觉也不难,简单来说,无论名称多么复杂,其实就是定义了一个类型,仅此而已。 - 第二,我在搜索素材的时候,发现大部分对象的类型,都是用
interface
定义的,而少部分需要用到交叉,联合等类型,才用type
定义,符合我们之前对比两者差异时总结的。
end
- 篇幅不易过长,本文到此结束,后续内容点击 用大白话讲 TypeScript,两小时快速上手TypeScript (下)