前端小王hs:
清华大学出版社《后台管理实践——Vue.js+Express.js》作者
网络工程师 前端工程师 项目经理 阿里云社区博客专家
email: 337674757@qq.com
vx: 文章最下方有vx链接
资料/交流群: vx备注Typescript
类型的特殊性
写在前头
虽然早已阅读完《Programming TypeSctipt》这本书,但一直以来没有时间好好的总结阅读时学到的新知识,同时由于要写关于Nest
的书籍中关于TypeScript
部分,以及想录制一套关于TypeScript进阶
的视频,所以趁现在花上几天时间总结一下TypeScript
的一些细节
本教程内容不是TypeScript
的入门教程,是对部分TypeScript
的细节进行归纳总结
注:本总结中TS
、ts
、TypeScript
都表示TypeScript
,js
、JavaScript
都表示JavaScript
为什么要学TypeScript
2025年,学习TypeScript
不是一种趋势、潮流,TypeScript
已成为前端开发的必学科目
从学习的结果来看:
- 大部分企业开发都需要用到
TypeScript
- 基于
JavaScript
的后端框架如Nest
需要用到TypeScript
- 鸿蒙
artks
是基于TypeScript
的,而鸿蒙很大可能是未来国内手机端软件开发的主流 - 加薪
从学习的过程来看:
TypeScript
可以加深对面向对象开发的理解,涉及到了多态
、继承
等内容- 学习
TypeScript
能够更好的理解MVC
,或者说衔接MVC
开发,因为需要从类型的角度去设计值 - 提高自己的代码水平,即减少后续的维护成本
TypeScript本质上就是给代码添加类型
TypeScript本质上就是给代码添加类型,并没有什么难度
学习TypeScript的几个目标
- 看得懂
TypeScript
的提示,并且能够根据提示进行修改,当然这在AI
时代不是什么难事 - 看得懂他人写的
TypeScript
代码(为什么他会这样写?这在维护他人代码的时候可是常见的问题) - 自己能够写的出好的
TypeScript
代码(能够以类型的角度去考虑需要解决的问题,尽量规避掉可能由于类型带来的隐患问题)
例如:如果没学好(没学完)TypeScript
,在面对大数值问题时,就可能会出现选择number
类型而不是bigint
类型
类型的特殊性
类型推导
unknown的"细化性"
当不确定某个值的类型时,应该选择unknown
,符合尽量少用any
的编程习惯,unknown
类型的值需要进行再次判断后(使用typeof
)才能执行,否则报错Object is of type 'unknown'
,代码如下所示:
let a:unknown = 30
if(typeof a === 'number'){ // 细化
let d = a + 10
}
实际的场景
通过API
获取的数据可能随着空间、时间或其他条件发生变化,那么应该考虑到要使用unknown
注意:unknown
必须显示注解
const的"具体性"
使用const
会将类型限制到取的具体值,当然这个取值是一定属于某个类型的,如下代码所示:
let a = true // boolean
var b = true // boolean
const c = true // true
为什么const会这样?
const
表示值不可变,TypeScript
编译器知道这个值永远不会变,所以做出更为精确的类型推断
const
同样适用于number
、bigint
、string
、下面仅以number
为例,代码如下:
let a = 123 // number
var b = Infinity * 0.10 // number
const c = 123 // 123
类型字面量(literal type)
const
的这种让常量确定唯一一个类型的,就叫类型字面量(或称为是字面量类型),这个类型可以是除基本数据类型( number
、string
、boolean
等)外的其他类型,如下面的具体的值也可以作为类型
const SUCCESS = 'success';
const ERROR = 'error';
在上面的例子中,TypeScript
会将SUCCESS
和ERROR
的类型推断为'success'
和'error'
这样的字面量类型,而不是更广泛的string
类型
const的实际应用场景
- 结合联合类型 + 字面量类型实现类型收窄(Discriminated Unions)
const SUCCESS = 'success'; // SUCCESS 的类型被推断为 'success'
const ERROR = 'error'; // ERROR 的类型被推断为 'error'
// 定义一个联合类型
type Status = typeof SUCCESS | typeof ERROR;
// 等价于 type Status = 'success' | 'error';
function setStatus(status: Status) {
if (status === SUCCESS) {
console.log('Operation was successful');
} else if (status === ERROR) {
console.log('Operation failed');
}
}
setStatus(SUCCESS); // 正确
setStatus('someOtherStatus'); // 错误:类型 '"someOtherStatus"' 不可分配给类型 'Status'
- 确定唯一环境变量
const API_BASE_URL = 'https://api.example.com/v1';
- 键值映射
const COLORS = {
PRIMARY: '#3498db',
SECONDARY: '#2ecc71'
} as const;
type ColorKey = keyof typeof COLORS; // "PRIMARY" | "SECONDARY"
type ColorValue = (typeof COLORS)[ColorKey]; // "#3498db" | "#2ecc71"
在这里可以看到const
对对象的使用又有区别,请继续看👇👇👇
在对象中使用const(难点)
在对象中使用const
,如下代码所示:
const Mode = {
EDIT: 'edit',
VIEW: 'view',
}
这种情况下,类型将会是:
{
EDIT: string;
VIEW: string;
}
原因在于TS
编译器认为js
的对象在创建之后值也有可能发生变化(地址不变),这其实很好理解,人的age
会随着时间发生变化,但DNA
不会
所以如果要将某个对象的值变为字面量类型,就需要加上类型断言,也就是as const
,代码如下:
const Mode = {
EDIT: 'edit',
VIEW: 'view',
} as const;
现在类型即为:
{
readonly EDIT: "edit";
readonly VIEW: "view";
}
symbol的三种作用
私有属性/防止属性名冲突
// 模块A定义一个 symbol 属性
const key = Symbol('id');
const user = {
name: 'Alice',
[key]: 123 // 私有的 ID
};
// 模块B不知道这个 key,不会误操作
for (let prop in user) {
console.log(prop); // 只输出 name
}
console.log(user[key]); // 输出 123
实现类的私有属性
const _secretKey = Symbol('secret');
class SecretBox {
[_secretKey]: string;
constructor(secret: string) {
this[_secretKey] = secret;
}
revealSecret() {
return this[_secretKey]; // 类内部拥有对_secretKey的引用,可以访问
}
}
const box = new SecretBox('TopSecret123');
console.log(box.revealSecret()); // 输出 TopSecret123
console.log(box[_secretKey]); // 类外部没有对_secretKey的引用,无法访问,'_secretKey' is not defined
console.log(box['secret']); // undefined
private私有的缺陷(使用类型断言绕过限制)(进阶细节)
ts
最终会被编译成js
,而js
并没有private
,所以能够使用类型断言绕过private
的私有限制访问到属性,如下示例代码所示:
class Person {
private secret: string;
constructor(secret: string) {
this.secret = secret;
}
getSecret(): string {
return this.secret;
}
}
const person = new Person('My Secret');
// 正常情况下,无法直接访问 private 属性
// console.log(person.secret); // 编译错误: Property 'secret' is private and only accessible within class 'Person'.
// 但是可以通过类型断言绕过这个限制
console.log((person as any).secret); // 输出: My Secret
// 或者通过索引访问的方式
console.log(person['secret']); // 输出: My Secret
结果如下所示:
绝对私有(ts3.8特性:#)(进阶)
TypeScript 3.8
开始引入了#
前缀用于声明真正的私有字段(Private Fields),这些字段不仅在类型检查阶段不可访问,在运行时也无法通过任何手段(包括类型断言和索引访问)访问到它们
class Person {
#secret: string;
constructor(secret: string) {
this.#secret = secret;
}
getSecret(): string {
return this.#secret;
}
}
const person = new Person('My Private Secret');
console.log(person.#secret); // 编译错误: Private field '#secret' must be declared in an enclosing class.
元编程
元编程指的是定义对象的行为,换句话说,能够改变原本对象的自有行为,以下面代码为例:
class MyType {
[Symbol.toStringTag] = 'MyCustomType';
}
const obj = new MyType();
console.log(Object.prototype.toString.call(obj));
// 输出: "[object MyCustomType]"
// 如果不加[Symbol.toStringTag] = 'MyCustomType';,那么输出的是"[object Object]"
// 清晰的看到改变了对象本身的属性
另外一种是给对象添加迭代器,让对象能够支持for...of
,这在ECMAScript 6 入门关于Iterator-接口一节中有说,代码如下:
const myIterable = {
data: [1, 2, 3],
[Symbol.iterator]() {
let index = 0;
const data = this.data;
return {
next() {
if (index < data.length) {
return { value: data[index++], done: false };
} else {
return { done: true };
}
}
};
}
};
for (const item of myIterable) {
console.log(item); // 输出 1, 2, 3
}
object与Objcet
object的无用性
书上有个例子特别有意思,如下代码所示:
let a: object = {
b: 'x'
};
a.b; // ❌ 报错:类型“object”上不存在属性“b”。
定义了a
是object
类型,但是访问a.b
报错,原因很简单,object
只是说a
是一个对象,但无法保证这个对象有b
这个属性
那要object
有什么用?
object的使用场景
例如某个通用函数需要接收一个对象参数,但不需要这个对象有什么属性,那么就可以用object
了,代码如下:
function logObjectType(obj: object) {
console.log('Received an object');
}
logObjectType({ name: 'Alice' }); // ✅ OK
logObjectType([1, 2, 3]); // ✅ OK
logObjectType(Math); // ✅ OK
logObjectType(123); // ❌ 错误:number 不是 object
这样做还有一个好处就是确定参数是object
是类型,说起来像是一句废话,但看下面的例子:
typeof new String('hello') === 'object' // true
typeof 'hello' === 'string' // true
function foo(x: object) {}
foo({}); // OK
foo(new String('abc')); // OK
foo('abc'); // 错误:string 不是 object
这里还可以延申一点自动装箱的知识点:
function printLength(str) {
console.log(str.length);
}
printLength('hello'); // 输出 5
printLength(new String('hello')); // 也能输出 5
我们知道对象才能调用方法,字符串为什么也有.length
?因为string
被临时变成了new String('hello')
这个对象,并在这个对象上调用了.length
object
确保某个值是一个对象而不是原始类型(即不是string
、number
、boolean
、bigint
、symbol
、null
或undefined
的值)
**注意:**在实际开发中更应该根据接收的对象类型来确定具体的类型,而不是使用object
Object
大写的Object
是所有JavaScript
对象的基础类型。它不仅包括普通对象,还包括所有继承自Object.prototype
的对象,比如数组、函数、日期等,如下代码所示:
let obj: Object;
obj = {}; // ✅ 正确
obj = []; // ✅ 正确
obj = () => {}; // ✅ 正确
obj = new Date(); // ✅ 正确
obj = 'hello'; // ✅ 正确:String 对象也是 Object
obj = 123; // ✅ 正确:Number 对象也是 Object
而如果是object
,就会爆红线了,如下图所示:
一般开发也不推荐用这么宽泛的类型
数字分隔符
let oneMillion = 1_000_000 // = 1000000
null、undefined、viod和never
null
:表示缺少值undefined
:尚未定义never
:函数不返回(一直执行,如while(true){}
,或异常,如抛出throw
)viod
:函数没有显式的返回值(例如console.log
)
除非是大公司,一般null
和undefined
混用十分常见
const enum和enum(面试)
首先是枚举的键、值都可以访问,如下图所示:
但有隐患的是,访问不存在的值,不会报错,如下图所示:
这就需要const enum
,加上之后就不允许这样了,不能够反向查找了,同时需要注意的是,运行时也不生成JS
代码,只会在需要的地方进行替换,如下图所示:
面试可能会问const enum
会不会生成js
代码,那么上述就是答案了
TSC标志
strict家族
类似noImplicitAny
之类的noImplicitXXX
的都是strict
家族,如果开启了strict
,那么noImplicitXXX
就不需要设置了,皆为true
noImplicitAny
开启之后将会对隐式的any
类型报错
静待补充…
一个关键的问题,为什么尽量让ts自行推导类型(面试)
任何关于TS
的书籍都会提到诸如不要使用let num:number = 100
这样显式注解方式去声明类型,为什么?
- 大家都一眼能看懂就没必要写了
- 写了还可能出错
TS
的推断能力能强- 会被上司质疑一晚上加班就干这个?
- 代码本身就很多了,还加这么多?
欢迎关注csdn前端领域博主: 前端小王hs,喜欢可以点个赞!您的支持是我不断更新的动力!
前端小王hs:
清华大学出版社《后台管理实践——Vue.js+Express.js》作者
网络工程师 前端工程师 项目经理 阿里云社区博客专家
email: 337674757@qq.com
wx:最下方有联系方式
docker交流: 加v备注TypeScript