NodeJS系列(14)- TypeScript (一) | 安装 TypeScript、TypeScript 常用类型

发布时间 2023-11-07 13:04:22作者: 垄山小站


JavaScript 现在是有史以来最广泛使用的跨平台语言之一。JavaScript 最初是一种用于向网页添加微不足道的交互性的小型脚本语言,现已发展成为各种规模的前端和后端应用的首选语言。 虽然用 JavaScript 编写的程序的大小、作用域和复杂性呈指数级增长,但 JavaScript 语言表达不同代码单元之间关系的能力却没有。 结合 JavaScript 相当特殊的运行时语义,语言和程序复杂性之间的这种不匹配使得 JavaScript 开发成为一项难以大规模管理的任务。

程序员编写的最常见的错误类型可以描述为类型错误: 在期望不同类型的值的地方使用了某种类型的值。 这可能是由于简单的拼写错误、未能理解库的 API 表面、对运行时行为的错误假设或其他错误。 TypeScript 的目标是成为 JavaScript 程序的静态类型检查器 - 换句话说,一个在代码运行之前运行的工具(静态)并确保程序的类型正确(类型检查)。

TypeScript 是一种基于 JavaScript 构建的强类型编程语言,可为你提供任何规模的更好工具。如果没有 JavaScript 背景的情况下使用 TypeScript,并且打算将 TypeScript 作为你的第一语言,建议首先开始阅读有关 Microsoft Learn JavaScript 教程 (https://developer.microsoft.com/zh-cn/javascript/) 或阅读 Mozilla 网络文档中的 JavaScript (https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide) 的文档。

TypeScript 可以通过三种安装路径进行安装,具体取决于你打算如何使用它:npm 模块、NuGet 包、或者 Visual Studio 插件。

本文介绍在 Node/npm 环境下安装 TypeScript 和 TypeScript 常用类型。

TypeScript:https://www.typescriptlang.org/(中文版:https://ts.nodejs.cn/)
Microsoft TypeScript: https://devblogs.microsoft.com/typescript/
GitHub:https://github.com/microsoft/TypeScript
NPMJS: https://www.npmjs.com/package/typescript


1. 系统环境

    操作系统:Windows 10 (x64)
    NVM:1.1.11
    NodeJS: 14.21.3 LTS
    NPM:6.14.18
    Typescript: 5.2.2
    工作目录:D:\workshop\nodejs


2. 安装 TypeScript

    1) 创建 npm 项目

        进入工作目录 D:\workshop\nodejs,手动创建子目录 tsdemo,在命令行控制台进入该子目录,运行如下命令:

            D:\workshop\nodejs\tsdemo> npm init

                ...

            注:可以运行带 -y 参数的命令 npm init -y,即出现交互选择时都自动选 yes。init 命令运行完成后,tsdemo 目录先生成一个 package.json 文件。

    2) 下载 TypeScript 到 npm 项目

        把 TypeScript 模块写入 devDependencies 节点,npm install 初始化时会自动下载模块。

            D:\workshop\nodejs\tsdemo> npm install typescript --save-dev

                ...

        或把 TypeScript 模块写入 dependencies 节点,npm install 初始化时会自动下载模块。

            D:\workshop\nodejs\tsdemo> npm install typescript --save

                ...

        或把 TypeScript 模块写入 dependencies 节点,npm install 初始化时不会自动下载模块,需要手动下载。

            D:\workshop\nodejs\tsdemo> npm install typescript

                ...

    3) 示例

        在 D:\workshop\nodejs\tsdemo 目录下创建 greeter.ts 文件,代码如下:

            function greeter(person) {
                return "Hello, " + person;
            }
            
            let user = "Jane User";

            if (typeof(document) === 'undefined') {
                console.log(greeter(user));    
            } else {
                document.body.textContent = greeter(user);
            }


            注: 在 Visual Studio 中打开 greeter.ts,可以将鼠标悬停在标识符(变量、函数名等)上以查看它们的类型。请注意,在某些情况下,这些类型是自动为你推断的。重新输入最后一行,并根据 DOM 元素的类型查看完成列表和参数帮助。将光标放在对 greeter 函数的引用上,然后按 F12 转到它的定义。还要注意,可以右键单击一个符号并使用重构来重命名它。

        运行 npx tsc 命令编译 greeter.ts 文件:

            D:\workshop\nodejs\tsdemo> npx tsc greeter.ts

            注:在同级目录下,生成了一个 greeter.js 文件。

        命令行方式运行 greeter.js,格式如下:

            D:\workshop\nodejs\tsdemo> node greeter.js

                Hello, Jane User

        浏览器方式运行 greeter.js,这里创建一个 greeter.html 文件,代码如下:

            <!DOCTYPE html>
            <html>
            <head>
                <title>TypeScript Greeter</title>
            </head>
            <body>
                <script src="greeter.js"></script>
            </body>
            </html>

        在浏览器中打开 greeter.html 文件,显示和命令行运行一样的内容。

    4) 安装 ts-node

        命令行直接执行 ts 文件,需要安装 ts-node,安装命令如下:

            D:\workshop\nodejs\tsdemo> npm install -g ts-node

        上文的 greeter.ts 文件,其实是一个 Javascript 格式的文件,不符合 TypeScript 的类型要求,需要改成如下格式:

            function greeter(person: string) {
                return "Hello, " + person;
            }
            
            let user: string = "Jane User";

            console.log(greeter(user)); 


        命令行方式运行 greeter.ts,格式如下:

            D:\workshop\nodejs\tsdemo> ts-node greeter.ts

                Hello, Jane User


3. TypeScript 常用类型

    JavaScript 有三个常用的基本类型: string、number 和 boolean。在 TypeScript 中都有对应的类型,可以使用 JavaScript typeof 运算符查看:

        string 表示字符串值,如 "Hello, world"
        number 代表像 12 这样的数字。 JavaScript 对整数没有特殊的运行时值,因此没有等价于 int 或 float - 一切都只是 number
        boolean 代表 true 和 false 这两个值

    类型名称 String、Number 和 Boolean(以大写字母开头)是合法的,但指的是一些很少出现在代码中的特殊内置类型。一般使用 string、number 或 boolean 作为类型。

    TypeScript 新增或不同于 JavaScript 的类型或类型概念:类型注解 (Type Annotation)、联合类型 (Union Type)、类型别名 (Type Alias)、接口 (Interface)、类型断言 (Type Assertion)、字面类型 (Literal Type)、字面推断 (Literal Inference) 等。

    1) 类型注解 (Type Annotation)

        使用 const、var 或 let 声明变量时,可以选择添加类型注解以显式指定变量的类型。声明函数时,可以在每个参数后面加上类型注解,声明函数接受哪些类型的参数,也可以给函数添加返回类型注解。示例代码如下:

            let myName: string = "Alice";       // 变量的类型注解

            // 函数参数和返回值的类型注解
            function greet(name: string): boolean {
                console.log("Hello, " + name.toUpperCase() + "!");
                return true;
            }

            console.log(greet(myName));

            注:在大多数情况下,不需要加类型注解。TypeScript 会尽可能地尝试自动推断代码中的类型。

        匿名函数与函数声明有些不同。当一个匿名函数出现在 TypeScript 可以确定如何调用它的地方时,该函数的参数会自动被赋予类型。示例如下:

            const names = ["Alice", "Bob", "Eve"];
            
            // 参数 s 不需要类型注解, 使用箭头函数或一般函数效果一样
            names.forEach((s) => {
                console.log(s.toUpperCase());
            });

 

    2) 联合类型 (Union Type)

        联合类型是由两种或多种其他类型组成的类型,表示可能是这些类型中的任何一种的值,这些类型中的每一种被称为联合类型的成员。

        使用 const、var 或 let 声明联合类型变量,格式如下:

            let id: number | string = 5;
            console.log(typeof(id));    // number

            id = 'abc';
            console.log(typeof(id));    // string

            id = { name: 'typescript' };
            console.log(typeof(id));    // object

            注:以上代码运行 npx tsc 命令时,编译系统会提示参数类型错误,因为 { name: 'typescript' } 是 Javascript 对象,不是 number 也不是 string,报错但不会影响在同级目录生成 js 文件。

        带联合类型参数的函数,格式如下:

            function printId(id: number | string) {
                if (typeof id === "string") {
                    // In this branch, id is of type 'string'
                    console.log("Your ID is: " + id.toUpperCase());
                } else {
                    // Here, id is of type 'number'
                    console.log("Your ID is: " + id);
                }
            }
            
            printId(100);       // Your ID is: 100
            printId('xyz');     // Your ID is: XYZ
            printId({ myID: 123 });     // Your ID is: [object Object]

            注:以上代码运行 npx tsc 命令时,编译系统会提示参数类型错误,因为 { myID: 123 } 是 Javascript 对象,不是 number 也不是 string,报错但不会影响在同级目录生成 js 文件。

    3) 类型别名 (Type Alias)

        通常我们在类型注解中编写对象类型或联合类型,它们一般有多个成员,比如上文的 number | string。这些对象类型或联合类型其实是我们的自定义类型,在同一个 ts 文件中可能会被重复使用,而且有时它们的成员数量可能比较多。显然,这会带来代码冗余、修改时需要同时修改多处代码等问题。

        于是,给我们的自定义类型取一个简短好记的别名,在某些情况下是一件很有用甚至必要的事情。

        基本类型的别名,格式如下:

            type stringType = string;

            let str: stringType = 'basic';
            console.log(typeof(str));    // string

            注:在类型注解里使用 stringType 别名,相当于使用 string,有点类似于语法糖,实际作用不大,所以基本类型一般很少使用别名。

        联合类型的别名,格式如下:

            type ID = number | string;
            
            let id1: ID = 101;
            console.log(typeof(id1));    // number

            let id2: ID = 'aaa';
            console.log(typeof(id2));    // string


        对象类型的别名,格式如下:

            type Point = {
                x: number;
                y: number;
            };
 
            // Exactly the same as the earlier example
            function printCoord(pt: Point) {
                console.log("type:x = " + pt.x);
                console.log("type:y = " + pt.y);
            }
            
            printCoord({ x: 100, y: 100 });

 

    4) 接口 (Interface)

        接口(interface)是命名对象类型的另一种方式,格式如下:

            interface Point2 {
                x: number;
                y: number;
            }
            
            function printCoord2(pt: Point2) {
                console.log("interface: x = " + pt.x);
                console.log("interface: y = " + pt.y);
            }
            
            printCoord2({ x: 200, y: 200 });


        就像我们在上面使用类型别名时一样,该示例就像我们使用匿名对象类型一样工作。TypeScript 只关心我们传递给 printCoord 的值的结构 - 它只关心它是否具有预期的属性。 只关心类型的结构和功能是我们称 TypeScript 为结构类型类型系统的原因。

        接口和类型别名非常相似,在很多情况下可以在它们之间自由选择。接口的几乎所有功能都在类型别名中可用,主要区别在于类型别名无法重新打开类型以添加​​新属性,而接口始终可以扩展。

    5) 类型断言 (Type Assertion)

        使用类型断言来指定更具体的类型,格式如下:

            const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;

            注:document.getElementById,TypeScript 只知道这将返回某种 HTMLElement,这里使用 as 来预先判断它是 HTMLCanvasElement 。

        与类型注解一样,类型断言被编译器删除,不会影响代码的运行时行为。还可以使用尖括号语法(在 .tsx 文件中),格式如下:

            const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");

            注:因为类型断言在编译时被删除,所以没有与类型断言关联的运行时检查。 如果类型断言错误,则不会产生异常或 null。

        TypeScript 只允许类型断言转换为更具体或更不具体的类型版本。此规则可防止 “impossible” 强制,例如:

            const x = "hello" as number;
            Conversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.

        有时,此规则可能过于保守,并且不允许可能有效的更复杂的强制转换。 如果发生这种情况,你可以使用两个断言,首先是 any(或 unknown,我们稍后会介绍),然后是所需的类型:

            const a = expr as any as T;

    6) 字面类型 (Literal Type)

        除了通用类型 string 和 number 之外,我们还可以在类型位置引用特定的字符串和数字。

        考虑这一点的一种方法是考虑 JavaScript 如何使用不同的方法来声明变量。 var 和 let 都允许更改变量中保存的内容,而 const 不允许。 这反映在 TypeScript 如何为字面创建类型。

            let changingString = "Hello World";
            changingString = "Olá Mundo";
            // Because `changingString` can represent any possible string, that
            // is how TypeScript describes it in the type system
            changingString;     // 它的类型注解是 string
            
            const constantString = "Hello World";
            // Because `constantString` can zhejieonly represent 1 possible string, it
            // has a literal type representation
            constantString;     // 它的类型注解是 "Hello World","Hello World" 成了字面类型


        就其本身而言,字面类型并不是很有价值:

            let x: "hello" = "hello";
            // OK
            x = "hello";
            // ...
            x = "howdy";    //  Type '"howdy"' is not assignable to type '"hello"'.


        变量只能有一个值并没有多大用处。但是通过将字面组合成联合,你可以表达一个更有用的概念 - 例如,只接受一组已知值的函数:

            function printText(s: string, alignment: "left" | "right" | "center") {
                console.log(s + ' -> ' + alignment);
            }

            printText("Hello, world", "left");
            printText("G'day, mate", "centre");     // Argument of type '"centre"' is not assignable to parameter of type '"left" | "right" | "center"'.


        数字字面类型的工作方式相同:

            function compare(a: string, b: string): -1 | 0 | 1 {
                return a === b ? 0 : a > b ? 1 : -1;
            }


        当然,你可以将这些与非字面类型结合使用:

            interface Options {
                width: number;
            }

            function configure(x: Options | "auto") {
                console.log(x);
            }

            configure({ width: 100 });
            configure({ height: 200 });     //  Argument of type '{ height: number; }' is not assignable to parameter of type '"auto" | Options'.
            configure("auto");
            configure("automatic");     // Argument of type '"automatic"' is not assignable to parameter of type 'Options | "auto"'.

        还有一种字面类型: 布尔字面量。 只有两种布尔字面类型,正如你可能猜到的,它们是 true 和 false 类型。类型 boolean 本身实际上只是联合 true | false 的别名。

    7) 字面推断 (Literal Inference)

        当你使用对象初始化变量时,TypeScript 假定该对象的属性可能会在以后更改值。例如,如果你编写如下代码:

            const obj = { counter: 0 };
            if (someCondition) {
                obj.counter = 1;
            }


        TypeScript 不假定将 1 分配给先前具有 0 的字段是错误的。 另一种说法是 obj.counter 必须具有 number 类型,而不是 0,因为类型用于确定读取和写入行为。

        这同样适用于字符串:

            function handleRequest(url: string, method: "GET" | "POST"): void {
                console.log(method + ' -> '  + url);
            }
            
            const req = { url: "https://example.com", method: "GET" };
            handleRequest(req.url, req.method);   // Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.
       


        在上面的例子中,req.method 被推断为 string,而不是 "GET"。 因为可以在 req 的创建和 handleRequest 的调用之间评估代码,这可以将一个新的字符串(如 "GUESS" 分配给 req.method),TypeScript 认为此代码有错误。

        有两种方法可以解决这个问题:

            (1) 可以通过在任一位置添加类型断言来更改推断:

                // Change 1:
                const req = { url: "https://example.com", method: "GET" as "GET" };
                // Change 2
                handleRequest(req.url, req.method as "GET");
            
                更改 1 表示 “我打算让 req.method 始终具有 _ 字面类型 _"GET"“,防止之后可能将 "GUESS" 分配给该字段。 更改 2 表示 “由于其他原因,我知道 req.method 的值为 "GET"“。

            (2) 可以使用 as const 将整个对象转换为字面类型:

                const req = { url: "https://example.com", method: "GET" } as const;
                handleRequest(req.url, req.method);
                
                as const 后缀的作用类似于 const,但用于类型系统,确保为所有属性分配字面类型,而不是更通用的版本,如 string 或 number。

    8) 其它

        (1) any

            TypeScript 有一个特殊的类型 any,当不希望某个特定的值导致类型检查错误时,可以使用它。

            当一个值的类型为 any 时,可以访问它的任何属性(这又将是 any 类型),像函数一样调用它,将它分配给(或从)任何类型的值,或者几乎任何其他东西这在语法上是合法的:

                let obj: any = { x: 0 };

                // None of the following lines of code will throw compiler errors.
                // Using `any` disables all further type checking, and it is assumed
                // you know the environment better than TypeScript.
                obj.foo();
                obj();
                obj.bar = 100;
                obj = "hello";
                const n: number = obj;


            如果没有指定类型,并且 TypeScript 不能从上下文推断它时,编译器通常会默认为 any。不过,通常希望避免这种情况,因为 any 没有经过类型检查。可以使用编译器标志 noImplicitAny 将任何隐式 any 标记为错误。

        (2) null 和 undefined

            JavaScript 有两个原始值用于表示值不存在或未初始化的值: null 和 undefined。TypeScript 有两个对应的同名类型。这些类型的行为取决于你是否启用了 strictNullChecks 选项:

                strictNullChecks 关闭: 可能是 null 或 undefined 的值仍然可以正常访问,并且值 null 和 undefined 可以分配给任何类型的属性。 这类似于没有空检查的语言(例如 C#、Java)的行为方式。 缺乏对这些值的检查往往是错误的主要来源; 如果在他们的代码库中这样做是可行的,我们总是建议人们打开 strictNullChecks。
                strictNullChecks 开启: 当值为 null 或 undefined 时,你需要在对该值使用方法或属性之前测试这些值。
                    
            就像在使用可选属性之前检查 undefined 一样,我们可以使用缩小来检查可能是 null 的值:

                function doSomething(x: string | null) {
                    if (x === null) {
                        // do nothing
                    } else {
                        console.log("Hello, " + x.toUpperCase());
                    }
                }


        (3) 非空断言运算符(后缀!)

            TypeScript 还具有一种特殊的语法,可以在不进行任何显式检查的情况下从类型中删除 null 和 undefined,即忽略 null 或 undefined 的空值警告。

                function liveDangerously(x: number | null) {
                    // No error
                    console.log(x!.toFixed());
                }

            需要谨慎使用 !,因为它绕过了 TypeScript 的类型检查,如果 x 实际值为 null 或 undefined 时,

        (4) 枚举

            枚举是 TypeScript 添加到 JavaScript 的一项功能,它允许描述一个值,该值可能是一组可能的命名常量之一。 与大多数 TypeScript 功能不同,这不是对 JavaScript 的类型级添加,而是添加到语言和运行时的东西。 正因为如此,这是一个你应该知道存在的功能,但除非你确定,否则可能会推迟使用。 你可以在 枚举参考页 中阅读有关枚举的更多信息。

        (5) bigint

            从 ES2020 开始,JavaScript 中有一个原语用于非常大的整数,BigInt:

                // Creating a bigint via the BigInt function
                const oneHundred: bigint = BigInt(100);
                
                // Creating a BigInt via the literal syntax
                const anotherHundred: bigint = 100n;


            可以在 TypeScript 3.2 发行说明 中了解有关 BigInt 的更多信息。

        (6) symbol

            JavaScript 中有一个原语用于通过函数 Symbol() 创建全局唯一引用:

                const firstName = Symbol("name");
                const secondName = Symbol("name");
                
                if (firstName === secondName) {
                    This comparison appears to be unintentional because the types 'typeof firstName' and 'typeof secondName' have no overlap.
                    // Can't ever happen
                }