TypeScript那些古怪又让人困惑的地方
JavaScript 因常被人诟病很复杂而臭名昭著。这主要是因为你可以用多种方式写出相同的东西,这往往会造出一堆混乱且无人维护的代码。
尽管TypeScript可以限制我们避免犯愚蠢的错误,但它也带来了一些它自身的问题和奇怪之处。今天我们就来看看一些让我觉得TS很有趣的怪事。
我们开始吧。
問號? VS 與與 && VS 或或 || VS 雙問號??先简单热个身。
TypeScript 提供了几种条件运算符 ?:
、??
、&&
和 ||
。下面介绍每一项:
**? :**
(三元运算符):根据条件返回两个表达式之一。
true ? "true" : "false"; // "真"
false ? "true" : "false"; // "假"
1 ? "true" : "false"; // "真"
0 ? "true" : "false"; // "假"
"hello" ? "true" : "false"; // "真"
"" ? "true" : "false"; // "假"
null ? "true" : "false"; // "假"
undefined ? "true" : "false"; // "未定义"
[] ? "true" : "false"; // "真"
{} ? "true" : "false"; // "真"
作为 if-else
语句的简写形式。
**& &**
(逻辑与(& &)):返回第一个假值,如果没有假值则返回最后一个真值。
// 经常用于条件渲染
isAuthenticated && "个人主页"; // "个人主页"
true && "是"; // "是"
false && "是"; // false
0 && "是"; // 0
1 && "是"; // "是"
null && "是"; // null
undefined && "是"; // undefined
"" && "是"; // ""
"hello" && "你好"; // "你好"
// 表达式链
// expression && (expression && expression)
null && undefined && "是" // null
"value" && undefined && "是" // undefined
"value" && "value" && "是" // "是"
这常用于根据条件进行渲染的情况。
**||**
(逻辑或运算符):返回第一个为真的操作数,如果没有找到为真的操作数,则返回最后一个操作数。
null || "默认"; // "默认"
未定义 || "默认"; // "默认"
假 || "默认"; // "默认"
真 || "默认"; // 真
0 || "默认"; // "默认"
1 || "默认"; // 1
"" || "默认"; // "默认"
"hello" || "默认"; // "hello"
// 连接
// 表达式 || (表达式 || 表达式)
null || 未定义 || "默认" // "默认"
"值" || 未定义 || "默认" // "值"
null || "值" || "默认" // "值"
它用来为任何假值(如 null
、undefined
、0
、false
、""
)提供替代值。
**??**
(空值合并运算符):只有当左操作数为null
或undefined
时,才返回右边的值。
// 只有这两种情况会返回 'Nullish'
null ?? "Nullish"; // "Nullish"
undefined ?? "Nullish"; // "Nullish"
true ?? "Nullish"; // true
false ?? "Nullish"; // false
0 ?? "Nullish"; // 0
1 ?? "Nullish"; // 1
"" ?? "Nullish"; // ""
"hello" ?? "Nullish"; // "hello"
// 链式用法
// 表达式 ?? (表达式 ?? 表达式)
null ?? undefined ?? "Nullish" // "Nullish"
"value" ?? undefined ?? "Nullish" // "value"
null ?? "value" ?? "Nullish" // "value"
(??:如果左边操作数为 nullish,则返回右边的操作数)
这用于确保 null
或 undefined
的变量有默认值,同时不会把类似 0
、false
或 ""
的值误认为是空值变量。
与其它语言相同,TS 也提供了多种转换类型的方法。
比如,这些是转换成数字的选项:
const num1 = Number("123"); // 将字符串转换为数字
const num2 = parseInt("123px"); // 123 (即 123)
const num3 = parseInt("123.45"); // 123 (即 123)
const num4 = parseFloat("123.45"); // 123.45 (即 123.45)
const num5 = +"123"; // 123 (即 123)
const num6 = +true; // 1 (即 1)
const num7 = +false; // 0 (即 0)
const num8 = +[]; // 0 (即 0)
const num9 = +[123]; // 123 (即 123)
你可以使用以下方法将它转换为 string:
your_variable = 'your_value'
str(your_variable)
const str1 = String(123); // 输出为 "123"
const str2 = 123.toString(); // 输出为 "123"
const str3 = `${123}`; // 输出为 "123"
const str4 = 1 + ''; // 输出为 "1"
const str5 = true + ''; // 输出为 "true"
const str6 = [1, 2, 3] + ''; // 输出为 "1,2,3"
以下是 boolean
的可用选项:
const bool1 = Boolean(123); // true (真)
const bool2 = Boolean(0); // false (假)
const bool3 = !!"hello world"; // true (真) // 双重否定结果为真 (The double negation results in true)
const bool4 = !!""; // false (假) // 空字符串的双重否定结果为假 (The double negation of an empty string results in false)
const bool5 = !!0; // false (假) // 0的双重否定结果为假 (The double negation of 0 results in false)
const bool6 = !![]; // true (真) // 空数组的双重否定结果为真 (The double negation of an empty array results in true)
const bool7 = !!{}; // true (真) // 空对象的双重否定结果为真 (The double negation of an empty object results in true)
任何 VS 未知数.
在 TypeScript 中,有两种方法可以避开类型安全的检查,
**任何**
: 彻底关闭类型检查,允许任意类型的值赋值或使用,没有任何限制
const value: any;
value = "Hello";
value = true;
// 这里没有类型检查,任何操作都是允许的
console.log(value.toUpperCase()); // 执行时会报错
**不确定类型**
:表示一种需要先进行类型检查或断言后才能使用的类型
const value: unknown;
value = "Hello";
value = true;
// 在执行任何操作之前,需要先检查变量的类型:
if (typeof value === 'string') {
console.log(value.toUpperCase());
}
简而言之,any
允许任何类型而无需进行任何检查,而 unknown
在使用前会强制进行类型检查。
另一个奇怪的地方在于,TypeScript 有两种类型用来表示值缺失:
**null**
:表示空值(值存在,但其值为null
)
let a = 4; // 初始化 a 为 4
a = null; // 将 a 设为 null
**未定义**
:表示未定义的值或缺失值,即值缺失
let a; // a是未定义的变量
当你访问一个不存在的对象的属性时,也会得到 undefined
。
const obj = {
value: 27,
};
// 定义一个对象obj,包含一个属性value,值为27
obj.Value; // 尝试访问大写的Value属性,结果为undefined,因为对象中没有这个属性
总之,null
表示没有值,而 undefined
表示值缺失或尚未被设置。
在函数返回时,也有一些挺有趣的类型。
**void**
:表示不返回任何东西。通常用来表示函数不返回任何东西的情况。
function logMessage(): void {
// 此函数不返回任何东西
console.log("控制台输出:此函数不返回任何东西");
}
**从不**
:表示一个函数永远无法正常完成(例如抛出错误或进入无限循环)。它表示该函数永远不会成功返回结果。
function throwError(): never {
throw new Error(\"此函数绝不会返回\");
}
枚举类型 VS 联合 VS 可迭代的联合 VS 对象类型等
像这样的简单常量集合在 TypeScript 中也可以用多种方式定义。
- 传统的枚举
排序顺序枚举 {
ASC = 'ASC',
DESC = 'DESC',
}
// 使用方法
const 顺序1 = 排序顺序.ASC;
const 顺序2 = 'ASC'; // 错误
const 顺序3 = 排序顺序.ASC; // 类型推断为: 排序顺序
— 传统枚举的方式
— TS 会在未明确指定数据类型时自动推断
— 给常量提供了一个“命名空间(namespace)”
— 确保类型安全
— 在 foreach
中使用枚举会进行两次迭代,因为存在反向映射
— 枚举的值可以是 number
或 string
,但不能是复杂的对象,例如 array
字面类型合并
type 排序方式 = 'ASC' | 'DESC';
// 使用示例
const 方式1: 排序方式 = 'ASC';
const 方式2: 排序方式 = 排序方式Enum.ASC; // 可以赋值为枚举值
— 类型无法推知
— 可以是不同类型的组合,例如 number
,string
,object
等
— 编译后消失,意味着打包后的体积更小
— 不能使用 foreach
进行迭代
- 可迭代的联合体类型
const sortOrders = ['ASC', 'DESC'] as const;
type SortOrder = typeof sortOrders[number]; // 'ASC' | 'DESC'
// 示例用法
const order: SortOrder = 'ASC'; // 示例中使用
for (const order of sortOrders) {
// 做些事情
}
——与上述相同,但在编译后不会消失;——可迭代
- 对象
const 排序顺序对象 = {
ASC: 'ASC',
DESC: 'DESC',
};
type 排序顺序 = typeof 排序顺序对象[keyof typeof 排序顺序对象];
// 示例使用
const order1: 排序顺序 = 'ASC';
const order2 = 排序顺序对象.ASC;
const order3 = 排序顺序对象.ASC; // 类型:联合类型('ASC')
— 类型不会被自动推断
— 既可以作为枚举使用,也可以作为常量使用
— 编译后的大小比枚举更小
— 只需要一次迭代
— 值可以是复杂的类型,例如 array
,object
等
不管 TypeScript 提供多少选择,我个人只用传统的枚举 🙃。
如果 VS 满足对于类型断言语句,你有以下关键字,例如:as
和 satisfies
,。
比如说,下一个界面是:
/*
用户信息接口定义了两个属性:姓名和年龄。
*/
interface UserDto {
// 姓名
name: 字符串;
// 年龄
age: 数字;
}
**当作为**
:检查类型重叠但不执行类型检查
// 所有字段都齐全
const user1 = {
name: 'John',
age: 18,
}
const res1 = user1 as UserDto; // OK
// 包含一个额外的字段
const user2 = {
name: 'John',
surname: 'Doe',
age: 18,
}
const res2 = user2 as UserDto; // OK
// 缺少一个字段
const user3 = {
name: 'John',
}
const res3 = user3 as UserDto; // OK
// 缺少一个字段且包含一个额外的字段
const user4 = {
name: 'John',
surname: 'Doe',
}
const res4 = user4 as UserDto; // 错误,缺少 'age',类型不匹配
// 所有必需字段都缺失
const user5 = {
surname: 'Doe',
}
const res5 = user5 as UserDto; // 错误,缺少 'name' 和 'age',类型不匹配
**满足**
:确保类型结构正确。如果缺少任何必需的属性或者结构不匹配,TypeScript 会抛出一个错误。
// 所有字段都齐全
const user1 = {
name: 'John',
age: 18,
}
const res1 = user1 satisfies UserDto; // OK
// 多了一个字段(如姓氏)
const user2 = {
name: 'John',
surname: 'Doe',
age: 18,
}
const res2 = user2 satisfies UserDto; // OK
// 缺少一个字段
const user3 = {
name: 'John',
}
const res3 = user3 satisfies UserDto; // 错误,缺少年龄
// 缺少一个字段且多了一个字段
const user4 = {
name: 'John',
surname: 'Doe',
}
const res4 = user4 satisfies UserDto; // 错误,缺少年龄
// 所有字段都不齐全
const user5 = {
surname: 'Doe',
}
const res5 = user5 satisfies UserDto; // 错误,缺少名字和年龄
如你所见,主要的区别在于第三种情况,概括起来就是:
- 当你想要声明一个类型并且不介意某些属性缺失或结构不严格一致时,使用
as
。 - 当你想要确保一个值满足特定类型的条件并在编译时捕获任何与缺少或错误属性相关的错误时,使用
satisfies
。
as
和 satisfies
在声明变量时,可以显式指定类型,也可以使用断言。下面我们就来仔细看看。这里需要注意,TS 指的是 TypeScript。
咱们用一样的界面。
// 用户信息接口 (yóugōnghù xìnxī jiémiǎo)
interface UserDto {
名称: string; // name
年龄: number; // age
}
最简单的情况是所有属性都存在时。此时,TS会顺利定义变量,我们只需关注变量类型。
const user1: UserDto = { // 类型: UserDto
name: 'John',
age: 19,
};
const user2 = { // 类型: UserDto
name: 'John',
age: 19,
} as UserDto;
const user3 = { // 类型: { name: string, age: number }
name: 'John',
age: 19,
} satisfies UserDto;
const user4 = { // 类型: { name: string, age: number }
name: 'John',
age: 19,
}
如果某些字段缺失,只有 as
运算符不会出现任何错误:
const user1: UserDto = { // 错误:缺少 'age'
name: 'John',
};
const user2 = { // 没问题;
name: 'John',
} as UserDto;
const user3 = { // 错误:缺少 'age'
name: 'John'
} 符合 UserDto;
如果所有字段都存在,但我们多了一个额外的字段,那么明确的类型和 satisfies
都会报错:
const user1: UserDto = { // 错误: 'surname' 不是 UserDto 的属性
name: 'John',
age: 19,
};
const user2 = { // 正确
name: 'John',
age: 19,
} as UserDto;
const user3 = { // 错误: 'surname' 不是 UserDto 的属性
name: 'John',
age: 19,
} satisfies UserDto;
也有时候,as
操作符会出现错误:
// 多了一个字段,还少了一个字段
const user1 = { // 错误:转换错误,数据类型不匹配
name: 'John',
surname: 'Doe',
} as UserDto;
// 所有字段都不存在
const user2 = { // 错误:转换错误,数据类型不匹配
surname: 'Doe',
} as UserDto;
Readonly<T> 和 Object.freeze() 以及 as const 有什么区别?
可以使用 Readonly<T>
、Object.freeze
和 as const
来实现不可变性:
**只读 <T>**
:一个 TypeScript 实用程序,在编译时将给定类型的全部属性变为只读。
const obj = {
a: {
b: 2,
},
};
const readonlyObj: Readonly<{ a: { b: number } }> = obj;
obj.a = 4; // 没问题
readonlyObj.a = 4; // 编译错误
readonlyObj.a.b = 3; // 嵌套的对象仍然能改
obj.a.b // 3, 原始对象也被改了
— 仅在编译时使用
— 禁止修改属性
— 仅使顶级属性成为只读
Object.freeze()
:是JavaScript中的一个方法,用于冻结对象.
const obj = {
a: {
b: 2,
}
};
const freezedObj = Object.freeze(obj); // 类型: Readonly<{ a: { b: number } }>
// Object.freeze(obj); - 同样有效
obj.a = 4; // 运行时错误,因为 JavaScript
freezedObj.a = 4; // 编译错误,因为 TypeScript
freezedObj.a.b = 3; // 嵌套对象的属性依然可以被修改
obj.a.b // ,原始对象已被修改
— 在运行时被使用
— 将对象标记为不可变
— 只进行浅冻结,也就是说,嵌套的对象还是可以被更改
**常量 (
const)**
:TypeScript 提供了允许你创建一个深层次的不可变类型的功能。
const obj = { // 类型 { readonly a: { readonly b: 2 } }
a: {
b: 2,
}
} as const;
obj.a = 4; // 编译错误
obj.a.b = 3; // 编译错误
// 下面的代码示例展示了对只读属性的修改尝试
— 在编译时被使用
— 创建一种不可变类型
— 创建一种深层不可变类型。所有嵌套对象都是只读的,防止对类型级别进行任何修改
请注意以下内容:这些仅适用于嵌套情况。
const obj1 = { a: { b: 2 } } as const ;
const obj2 = { a: Object.freeze({ b: 2 }) };
typeof、instanceof、is、in、keyof 这几个关键字的区别
在 TypeScript 中,有多种方法可以进行类型验证,例如 typeof
、instanceof
、is
和 in
。
下面来分解一下它们之间的差别。比如我们来看一下下面的代码。
// 二维点类
class Point2D {
x: number = 0;
y: number = 0;
}
// 三维点类
class Point3D extends Point2D {
z: number = 0;
// 获取z坐标
getZ = () => this.z;
}
const point = new Point3D();
**typeof**
:返回表示操作数类型的字符串,比如'string'
,'number'
,'boolean'
,'object'
,'function'
,或'undefined'
const objType = typeof point; // 类型为 'object'
const isObject = typeof point === 'object'; // 表示 point 是对象
**instanceof**
:判断一个对象是否属于某个特定的类
const isPoint2D = point instanceof Point2D; // 真
const isPoint3D = point instanceof Point3D; // 真
const isString = point instanceof String; // 假
它仅适用于对象,而不能用于检查基本类型如 number
、string
或 boolean
。
const num: any = 4;
const 是数字 = num instanceof Object && typeof num === 'number'; // false 😔(真郁闷)
**是**
:作为函数的返回值类型
function isPoint3D(value: any): value 是 Point3D 类型 {
return value.z 不是 undefined;
}
这个函数会返回一个 boolean
值,但是 TypeScript 认识到当是 true
时,参数就会有一个更具体的类型。
const anyObject: unknown = point;
anyObject.getZ() // '.getZ()' 不可使用
const isPoint = isPoint3D(anyObject); // 真的
if (isPoint) {
anyObject.getZ(); // '.getZ()' 可用了。TS 认为它是一个 Point3D
}
anyObject.getZ(); // 在这里,'.getZ()' 又不可用了
**有**
:检查对象中是否有某个属性
// 检查point对象中是否存在名为'getZ'的属性,存在则为真
const getZExist = 'getZ' in point; // 真
// 检查point对象中是否存在名为'x'的属性,存在则为真
const xExist = 'x' in point; // 真
// 检查point对象中是否存在名为'test'的属性,存在则为假
const testExist = 'test' in point; // 假
// "const"是JavaScript中的关键字,用于声明常量
// "point"代表一个对象,这里表示一个点或坐标系中的一个点
**keyof**
:获取给定类型的所有键的并集
// TypeScript 中只能将 keys 设置为 ('x','y', 'z', 'getZ') 中的一个值
const keys: keyof Point3D = 'x';
它可以作为一个通用的限制。
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
getValue(point, 'z'); // 没问题
getValue(point, 'test'); // 编译错误,因为 'test' 不是 Point3D 的有效属性
注意:getValue
是一个泛型函数,其中 <K extends keyof T>
表示 key
必须是 T
类型对象的有效属性。例如,在 Point3D
中,有效的属性有 'x'
, 'y'
, 'z'
,而 'test'
就不是有效的属性。
甚至可以用它来定义类型。
// 创建联合类型来创建一个联合类型 ('x','y', 'z', 'getZ')
type Point3DKeys = keyof Point3D;
const keys: Point3DKeys = 'x';
// 创建映射类型,以下是一个示例类型定义
/*
如下所示的类型:
{
readonly x: number;
readonly y: number;
readonly z: number;
readonly getZ = () => number;
}
*/
type ReadonlyPoint3D = {
readonly [K in keyof Point3D]: Point3D[K];
};
const point: ReadonlyPoint3D = new Point3D();
将 point.x 设置为 4; // 错误,该属性为只读
接口 vs 类型别名
在其他语言中,接口 用于为类的层次结构声明行为,相比之下,在 TypeScript 中,通常用它来声明 DTO。
//接口定义
interface UserDto {
//姓名: 字符串
name: string;
//年龄: 数字
age: number;
}
事实上,你也可以对 type
做相同的操作。
type UserDto = {
name: string;
年纪: number; // 年纪表示用户的年龄
}
它们都可以是空的。
接口 UserDto { }
类型 UserDto = {};
它们可以利用工具类型来获益。
interface EmptyUserDto extends Omit<UserDto, 'name' | 'age'> { } // 空的用户数据传输对象,继承 UserDto 并排除 'name' 和 'age' 属性
type EmptyUserDto = Omit<UserDto, 'name' | 'age'>; // 定义空的用户数据传输对象,排除 'name' 和 'age' 属性
他们通过某种方式支持继承功能:
/**
* `PersonDto` 接口继承 `UserDto` 接口,表示一般用户的数据传输对象。
* `AdminDto` 接口继承 `UserDto` 接口,并添加了一个角色属性,表示管理员用户的数据传输对象。
*/
interface PersonDto extends UserDto { }
interface AdminDto extends UserDto {
role: string; // 角色
}
/**
* `PersonDto` 类型定义为 `UserDto` 类型,表示一般用户的数据传输对象。
* `AdminDto` 类型定义为 `UserDto` 类型与含有角色属性的对象的交集类型,表示管理员用户的数据传输对象。
*/
type PersonDto = UserDto;
type AdminDto = UserDto & {
role: string; // 角色
}
这也可以通过类来实现。
interface IUser {
name: string;
}
class User implements IUser {
name: string = 'John';
}
type TUser = {
name: string;
}
class User implements TUser {
name: string = 'John';
}
除了语法的不同之外,我能注意到的唯一两个明显的不同之处。
interface
必须有固定的属性,而type
则不必。
type 只读<T> = {
readonly [P in T 的键名]: T[P];
}; // 只读类型定义
TS会把多个接口声明合为一个。
interface UserDto {
name: string;
age: number;
}
// 上面定义了UserDto接口
interface UserDto {
role: string;
}
// 如需实现所有属性
const user: UserDto = {
name: 'John',
age: 18,
role: 'admin',
}
这在你想要扩展第三方代码的功能时很有用,但也可能在有人无意中引入冲突或意外情况时变得危险。改为:
这在你想要扩展第三方代码的功能时很有用,但也可能在有人无意中引入冲突或意外情况时变得危险。改为:
这在你想要扩展第三方代码的功能时很有用,但也可能在有人无意中引入冲突或意外情况时危险。
问题来了,你选择的是哪一个?🤔
由于可以互换,这取决于个人习惯或项目约定。有些人偏爱仅用 type
来表示类型别名。另一些人则仅在经典面向对象编程中使用 interfaces
来描述类的层次结构。没有绝对的正确答案。
我还是在处理接口相关的事情,因为我这老脑筋转不过这些新玩意儿 🙃.
最后正如你所见,不管TS有多么有帮助,它也有自己的缺点和弱点。你也是那些花了时间学习它,却发现它只是JavaScript的一个更强大的版本的人之一吗 😅。
💬 在评论区告诉我,你们多久会遇到 TypeScript 特性上的难题。还有哪些话题值得分享?你是「类型支持者」还是「接口支持者」?让我们一起讨论
✅ 订阅
⬇️ 查看我的其他文章
☕️ 点击下方链接支持我一下
👏 别忘了点赞哦
🙃 继续保持优秀
共同学习,写下你的评论
评论加载中...
作者其他优质文章