原始类型与对象类型 🚀
原始类型 🧱
嘿!JavaScript 的原始类型有 8 种(感谢 ES2020 给我们带来的 BigInt 👏),这些类型在 TypeScript 中都有对应的类型注解。除了 null
和 undefined
这两个"特殊宝贝"外,其他类型基本上与 JavaScript 中的数据类型概念一一对应。
const name: string = 'renouc'; // 字符串,你好呀~ 👋
const age: number = 24; // 数字,计算必备 🔢
const male: boolean = false; // 布尔值,不是 true 就是 false 🤔
const undef: undefined = undefined; // 未定义,空空如也 🌫️
const nul: null = null; // 空值,有盒子但没东西 📦
const obj: object = { name, age, male }; // 对象,万物皆对象 🧰
const bigintVar1: bigint = 9007199254740991n; // 超大整数,大到天花板都不够高 🚀
const bigintVar2: bigint = BigInt(9007199254740991);
const symbolVar: symbol = Symbol('unique'); // 独一无二的值 ✨
null 和 undefined 🤷♂️
在 JavaScript 中,null
和 undefined
是两兄弟,但性格不同 - null
表示"这里有值,但是个空值"(就像空盒子 📦),而 undefined
表示"根本没有值"(连盒子都没有 🌫️)。在 TypeScript 中,它们都是有明确意义的类型。
当你没有开启 strictNullChecks
这个"严格模式"时,它们会被当作其他类型的"小弟"(子类型),比如 string 类型会包含 null 和 undefined:
const tmp1: null = null;
const tmp2: undefined = undefined;
// 下面这两行代码只有在你"放松警惕"(关闭 strictNullChecks)时才行得通 😉
const tmp3: string = null; // 仅在关闭 strictNullChecks 时成立
const tmp4: string = undefined; // 仅在关闭 strictNullChecks 时成立
不过,开启 strictNullChecks
(强烈推荐!👮♀️)后,TypeScript 就不再让你随便把 null 或 undefined 赋值给其他类型了,除非你明确说"我允许"。这就像严格的门卫,不让不速之客进门 🚫。
void 🕳️
在 JavaScript 中,void
是个操作符,它的工作就是执行表达式但把结果扔掉,永远返回 undefined。有点像你做了一堆工作然后装作什么都没发生的样子 😎。
- 基本语法长这样:
void expression;
// 或者这样也可以
void expression;
无论 expression 是什么复杂的表达式,void 都会冷酷地返回 undefined 🥶。
- 它有什么用?看这里:
阻止链接跳转(超实用!)
<!-- 点击链接不会跳走,就像被粘住了一样 🍯 -->
<a href="javascript:void(0);">点我点我,我不会跑!</a>
<!-- 点击执行函数但页面纹丝不动 -->
<a href="javascript:void(someFunction());">做点事但别跳转</a>
立即执行函数表达式(IIFE)- 前端老炮儿都懂的技巧 😏
// 传统写法
void (function () {
console.log('我立刻就执行了!');
})();
// 现代写法(更常见)
(function () {
console.log('我也立刻执行了,但我更时髦!');
})();
在 TypeScript 中,void
摇身一变成了类型,表示函数不关心返回什么或压根儿不返回:
function fn1(): void {} // 啥也不返回 🤐
function fn2(): void {
return; // 空手而归 🙌
}
function fn3(): undefined {
return undefined; // 明确地返回了"无" 👻
}
fn1
和fn2
的返回值类型是 void
,而fn3
明确返回了 undefined
。
虽然 fn3 返回 undefined,但你还是可以用 void 标注它,因为在类型的世界里,它们都是在说"没啥有用的东西返回" 🤷♂️。主要区别:void 是"我不在乎返回值",undefined 是"我明确返回了个空气"。
你可以把 void 想象成一个黑洞 🕳️,而 null 和 undefined 是实实在在的值。有趣的是,undefined 可以赋给 void 类型(就像你可以往黑洞里扔东西),但反过来不行:
const voidVar1: void = undefined; // 完全没问题 👌
const voidVar2: void = null; // 除非关闭 strictNullChecks,否则会报错 ❌
数组 📚
在 TypeScript 中,声明数组有两种"口味":
const arr1: string[] = []; // 更常见的方式,像读英语一样直观 👍
const arr2: Array<string> = []; // 用泛型的高级写法,有些场景更清晰 🧐
元组 📏
当普通数组不够用时,元组(Tuple)来救场!它就像一个"量身定制"的数组,你可以精确规定每个位置的类型:
// 普通数组:类型相同,长度随意
const arr3: string[] = ['王', '富', '贵'];
console.log(arr3[599]); // TypeScript不会拦你,但运行时会得到undefined 😅
这时元组就派上用场了,它会严格检查索引访问:
// 元组:固定长度,每个位置类型可以不同
const arr4: [string, string, string] = ['王', '富', '贵'];
console.log(arr4[599]); // TypeScript直接报错:别想越界访问!🚨
你甚至可以把元组的某些位置标记为可选的(带问号):
const arr6: [string, number?, boolean?] = ['王富贵'];
// 也可以这么写,逗号更明显
// const arr6: [string, number?, boolean?] = ['王富贵', , ,];
对于可选成员,在严格模式下,它的类型会被看作 类型 | undefined
的联合类型。此时元组的长度也变得灵活了:
type TupleLength = typeof arr6.length; // 1 | 2 | 3,长度可变 🔄
TypeScript 4.0 还加入了具名元组,让你的代码可读性爆表 📈:
// 每个元素都有名字,看着就明白是啥
const person: [name: string, age: number, male: boolean] = ['王富贵', 25, true];
对象 🏗️
在 TypeScript 中,描述对象结构有两种主要方式:接口(interface)和类型别名(type)。接口就像是建筑蓝图,规定了对象需要长啥样:
interface IDescription {
name: string; // 必须有名字
age: number; // 必须有年龄
male: boolean; // 必须有性别标识
}
const obj1: IDescription = {
name: '王富贵',
age: 25,
male: true,
};
这意味着:
- 每个属性必须类型匹配,就像拼图一样要对上 🧩
- 不能随便加属性或少属性,无论是直接声明还是后续赋值,都要遵守蓝图!
给属性加"修饰符" 🎨
有时候,我们需要让某些属性变得"特殊"一点:
可选属性(加个问号,瞬间变随意):
interface IDescription {
name: string; // 这个必须有
age: number; // 这个也必须有
male?: boolean; // 可有可无,看心情 🤔
func?: Function; // 也是可选的
}
const obj2: IDescription = {
name: '张三',
age: 25,
// male和func没写,但没关系,它们是可选的~ 😌
};
只读属性(加个readonly,变得"看不改"):
interface IDescription {
readonly name: string; // 只能看不能改!
age: number;
}
const obj3: IDescription = {
name: '张三',
age: 25,
};
// 想改名字?不存在的!TypeScript会阻止你 🛑
obj3.name = '李四'; // 错误:name是只读的,别想动它!
数组和元组也能标记为只读,但与对象不同:
- 整个数组/元组才能标记为只读,不能只锁定某个元素 🔒
- 一旦标记为只读,push、pop等修改方法全部失效,数组变成了"看起来像数组但不能动"的
ReadonlyArray
// 只读数组,别想改我!
const readonlyArr: readonly string[] = ['a', 'b', 'c'];
// 或者这么写
const readonlyArr2: ReadonlyArray<string> = ['a', 'b', 'c'];
// 想push?没门!🚪
readonlyArr.push('d'); // 错误:ReadonlyArray上没有push方法
type vs interface 🥊
这是一场有趣的"类型擂台赛"!
interface
主要用来描述对象和类的结构,像是制定规范;而 type
更灵活,可以给各种类型起别名,组合出复杂类型,简直是"变形金刚" 🤖:
// interface:适合描述对象结构
interface User {
name: string;
age: number;
}
// type:百变星君,各种类型都能定义
type UserId = string; // 类型别名
type UserOrAdmin = User | Admin; // 联合类型,二选一
type GetUser = () => User; // 函数类型
type UserRecord = Record<string, User>; // 字典类型
主要区别(选择困难症看这里 👀):
interface
能被继承和实现,像是家族传承interface
能合并声明,多次定义会自动合并type
支持联合类型、交叉类型等高级操作,更灵活type
能为基本类型和复杂类型起别名
虽然很多时候 type
能替代 interface
,但根据团队约定和实际需求选择适合的方式才是正道 🧘♂️。
object vs Object vs {} 🤯
这三个容易混淆的类型,让人头大!下面帮你区分清楚:
- Object(大写O):万物之源,几乎接受一切类型(关闭严格检查时甚至接受null和undefined):
// 在关闭 strictNullChecks 时有效
const tmp1: Object = undefined;
const tmp2: Object = null;
const tmp3: Object = void 0;
// 以下在任何情况下都有效,Object真的很包容
const tmp4: Object = '王富贵';
const tmp5: Object = 599;
const tmp6: Object = { name: '王富贵' };
const tmp7: Object = () => {};
const tmp8: Object = [];
- 装箱类型(String, Number等):原始类型的"豪华包装版",不推荐使用 🙅♂️:
// 在关闭 strictNullChecks 时有效
const tmp9: String = undefined;
const tmp10: String = null;
// 只接受字符串和String对象
const tmp12: String = '王富贵';
const tmp13: String = new String('王富贵');
// 其他类型?没门!
const tmp14: String = 599; // 错误!
const tmp15: String = { name: '王富贵' }; // 错误!
最佳实践:别用这些装箱类型,它们只会让生活更复杂!用小写版本(string, number等)就好 👍
- object(小写o):专门表示非原始类型,也就是对象、数组、函数这一家子:
// 原始类型统统拒绝!(关闭 strictNullChecks 后可以)
const tmp20: object = '王富贵'; // 错误!
const tmp21: object = 599; // 错误!
const tmp22: object = true; // 错误!
// 非原始类型欢迎光临!
const tmp23: object = { name: '王富贵' }; // ✅
const tmp24: object = () => {}; // ✅
const tmp25: object = []; // ✅
- {}(空对象类型):看似啥都没有,实际上接受任何非null/undefined的值,是个"陷阱" 🕳️:
// 在关闭 strictNullChecks 时才有效
const tmp26: {} = undefined;
const tmp27: {} = null;
// 惊喜:几乎什么都能赋值!
const tmp28: {} = '王富贵'; // ✅
const tmp29: {} = 599; // ✅
const tmp30: {} = { name: '王富贵' }; // ✅
但是!虽然能赋值,却不能访问或修改任何自定义属性,这很"坑" 😱:
const obj: {} = { name: '王富贵' };
// 以下全部报错!
console.log(obj.name); // 错误:{}上不存在属性"name"
obj.age = 18; // 错误:{}上不存在属性"age"
这是因为 {}
类型只包含所有类型共有的方法(如 Object 原型上的 toString),自定义属性全部不可见。
最佳实践 💡
为了让代码更健壮,记住这几条:
- 永远不要用
Object
或String
这种大写的装箱类型,不值得 🚫 - 想表示非原始类型但不确定具体是啥,用
object
- 更推荐用这些精确的类型:
Record<string, unknown>
表示对象(更安全)unknown[]
表示数组(...args: any[]) => any
表示函数
- 避开
{}
这个陷阱,它几乎和any
一样危险 ⚠️
掌握了这些,你的 TypeScript 技能已经 +10 了!开心编码吧!😄