为什么要有 TypeScript?让 JS 告别 “薛定谔的 Bug”

6 阅读7分钟

前言

如果你点的椰果奶茶被做成了珍珠奶茶,虽然也能喝,但就是完全不是你想要的,至少对于我这种有点强迫症的人。那么 JavaScript 就是这样一个 “随性”奶茶店老板,而 TypeScript 就是那个拿着订单反复跟你确认 “少糖少冰”靠谱店员,从根源上避免了 “错单” 的尴尬。

用一句话来说其实就是:TypeScript 是更严谨的 JavaScript

一、有了 JavaScript 为什么还要有 TypeScript ?

JavaScript 就像开盲盒,你永远不知道下一个变量里装的是 数字字符串还是 薛定谔的 undefined。我统称它们为 盲盒变量

比如这段代码:

let n = 1, m = 0;
n = 'hello'; // 数字秒变字符串,JS 主打一个“灵活”

function add(a, b) {
    if (typeof a === 'number' && typeof b === 'number') {
        return a + b;
    }
}
// 传入字符串,函数直接返回 undefined,Bug 这不就来了
console.log(add(1, '2')); 

image.png

你以为你在写 “动态灵活” 的代码,其实是在给未来的自己 埋雷。比如上面这段代码,可能你知道等会要传 2个number类型,但是如果别人直接拿来Ctrl + cv 用你封装的函数,传了一个 string类型 那就坏了。

直到 TypeScript 出现,让变量从 “盲盒” 变成了 “明码标价的商品”

二、弱类型:自由过了火就是混乱

在上面代码中有这样一个情况:

let n = 1;
n = 'hello';   // 在 JavaScript里面不报错

如果你是 C++、Java或者Go的工程师,你肯定会觉得这人怕不是敲代码敲疯了吧。在C++、Java或者Golang里面这代码直接就报错了。这就是因为 JavaScript 是典型的弱类型语言,变量不需要提前声明类型,随时可以 “变身”

打印结果为 hello

image.png

你可以让数字 a 一秒变成字符串编辑器连个警告都没有。这种 “自由” 在小项目里或许能跑,但项目一复杂,就会出现 add(1, '2') 这种隐蔽 Bug,排查起来堪比大海捞针。

三、强类型:给变量上 “户口”

TypeScript 作为 JavaScript超集,核心就是给变量加上了类型声明

首先要用 TypeScript,我们需要去下载它:

npm install -g typescript   # 全局安装 TypeScript
tsc -v  # 查看 TypeScript的版本
tsc project.ts  # project 是你的文件名,编译 TypeScript文件

当编译完你会发现编译器给你编译出了一份对等的JavaScript文件:

image.png

这个时候你就可以用Node.js去跑这份文件,因为 TypeScript 本身不能直接运行,需要先编译成 JavaScript 再运行。不过现在有一些工具可以简化这个过程,比如 ts-nodedeno 等。

我这里简要介绍下 ts-node的使用:

ls package.json  # 首先检查项目是否有 package.json 文件
npm init -y  # 如果没有,初始化一个

npm install -g ts-node  # 然后安装 ts-node
npm install --save-dev ts-node  # 或本地安装

# 运行 TypeScript文件
ts-node 2.ts # 全局安装时
npx ts-node 2.ts  # 本地安装时

一般 TypeScript 都是在 React项目 等环境下运行,所以直接运行一个文件的比较少见,这里我们主要看 TypeScript 语法的使用和基础知识。

同样的代码,在 TypeScript 里面就会报错:

let a: number = 1; 
a = 'hello'; // 编辑器直接标红:不能将类型 “string” 分配给类型 “number” 
console.log(a);

image.png

细心的你很快就发现了猫腻:TypeScript 相较于 JavaScript 不同的地方就在于 TypeScript 的写法中明确标注了变量是什么类型。就比如这里 a 被明确声明为 number 类型,如果你想把它改成字符串TypeScript 会立刻报错,把问题扼杀在编码阶段。这样就使得文件更加严谨。

四、TypeScript 数据类型全家桶

在之前我写过几篇 JavaScript数据类型 的文章,那我们现在来看看 TypeScript 类型全家桶,他们并不完全一样,但还是有很高的相似度。

比如这段代码:

let isDone: boolean = false;   // boolean类型
let count: number = 123;       // number类型
let str: string = 'Trae';      // string类型
const symbol: symbol = Symbol();  // symbol类型
let obj: object = {
    [symbol]: 'Trae'          // object类型(对象)
};
let list: number[] = [1, 2, 3];  // array类型(数组)

enum Color {
    Red,
    Green,                   // 类似于结构体
    Blue
}
let color: Color = Color.Red;

let notSure: any = 10;      // any类型
notSure = '123'; // any 类型可以随便变,是 TypeScript 里的 “漏网之鱼”

let value: unknown = 10;   // unknown类型
value = '123';
let abc: string = 'hello';  

// unknown 类型不能直接赋值给其他类型,比 any 更安全
// abc = value;  // 报错
abc = notSure;   // 不报错

let tuple: [number, string] = [10, 'hello']; // 元组:固定长度和类型的数组

function user1(): number {
    return 123;
}
function user2(): Function {
    return function fn(): number {
        return 123;
    }
}
// 报错
// function user2(): string {
//     return 123;
// }

function user3(): void {} // void 表示没有返回值

let u: undefined = undefined;  // undefined类型
let n: null = null;            // null类型

基本上都与 JavaScript 相似,可以去看我之前写的 JavaScript数据类型。从基础的 booleannumberstring,到复杂的 enumtupleunknownTypeScript 让每个变量都有了明确的 “身份”

这里有一个注意的点就是 unknown 类型any类型unknown 类型不能直接赋值给其他类型,而 any 类型可以随便变,所以下次报错的时候看看,是不是这个原因。

image.png

五、对象与类型:不是所有空对象都一样

TypeScript 对对象的类型约束更严格:

const obj: object = {};
const obj2: Object = {};
const obj3: {} = {};

// 错误
// obj.a = 1;    // 编译错误
// obj3.a = 1;   // 编译错误

// 正确(类型断言)
(obj2 as any).a = 1;
console.log(obj2); // 输出: { a: 1 }

const hello = 'hello';
const a: 'hello' = 'hello';

objectObject{} 看似相似,实际约束力度不同;字面量类型更是把变量锁死在特定值上,杜绝了 “意外变身”。

image.png

六、类型守卫🛡️:给你的代码装上 “火眼金睛”

TypeScript 的类型守卫,就像给你的代码配上了一个智能安检员,能在运行时精准识别变量类型。

// 类型守卫
interface Person {
    name: string;
    age: number;
    sex?: unknown; // 可选属性,不是每个人都需要填写
}

const person: Person = {
    name: 'henry',
    age: 18,
    sex: '男' // 可选属性,写不写都不会报错
};

// 举个类型守卫的例子:判断一个值是不是 Person 类型
function isPerson(value: unknown): value is Person {
    return (
        typeof value === 'object' &&
        value !== null &&
        'name' in value &&
        'age' in value
    );
}

function printUserInfo(value: unknown) {
    if (isPerson(value)) {
        // 进入这个分支后,TypeScript 就知道 value 是 Person 类型了
        console.log(`姓名:${value.name},年龄:${value.age}`);
        if (value.sex) {
            console.log(`性别:${value.sex}`);
        }
    } else {
        console.log('这不是一个合法的 Person 对象');
    }
}

printUserInfo(person); // 输出:姓名:henry,年龄:18
printUserInfo({ name: 'lucy' }); // 输出:这不是一个合法的 Person 对象

七、类型转换与组合:灵活不代表放纵

如果遇到类型不确定的场景,TypeScript 提供了类型断言来 “手动担保”:

let someValue: any = '123';
let strLength = (someValue as string).length; // 写法一
let strLength2 = (<string>someValue).length;  // 写法二

还可以用 type 定义联合类型和交叉类型:

type Person = string | number | boolean;
const a: Person = 'hello';
const b: Person = 123;
const c: Person = true;

type PartialX = {x: number}
type Point = PartialX & {y: number}  // 交叉类型:合并多个类型
const p: Point = {
    x: 10,
    y: 20
}

八、泛型:写一次,适配所有类型

泛型TypeScript“秘密武器”,让函数和组件更通用。

function identity<T>(value: T) {
    return value;
}
identity<number>(100); // 指定 T 为 number 类型

function identity2<T, U>(value: T, msg: U): T {
    console.log(msg);
    return value;
}
identity2<number, string>(100, 'hello'); // 多泛型参数

let arr: Array<number> = [1, 2, 3];
let arr2: Array<number | string> = [1, 2, 3, 'hello'];

泛型让 identity 函数既能处理数字,也能处理字符串,不用写多个重复函数,代码复用性直接拉满。

结语

JavaScript 的 “盲盒变量”TypeScript 的 “精准类型”,本质是从 “靠运气写代码” 到 “靠逻辑写代码” 的转变。写的代码都不严谨,那还写什么代码呢😄。

TypeScript 不是给你套枷锁,而是给你装护栏 —— 它不会限制你的创造力,只会帮你提前避开那些低级 Bug。所以,不要害怕红色的报错,而是试着去解决它。

我的 JavaScript数据类型文章:栈与堆的精妙舞剧:JavaScript 数据类型深度解析