跳转至

TypeScript 的变量与变量类型

TypeScript 最大的特点就是引入了类型系统,这样就可以在编译为 JavaScript 代码之前由编译器进行类型检查。在这样的条件下,TypeScript 中的变量在声明的时候就可以指定类型,编译器在将 TypeScript 代码编译为 JavaScript 代码的时候会进行类型检查,若有不符合类型声明的情况则会报错:

1
2
3
4
5
const foo = (name: string): void => {
    console.log("Hello, " + name);
}

foo(2); // Error!

变量类型

由于 TypeScript 是 JavaScript 的超集,所以其基础变量类型依然和 JavaScript 一致。

声明变量时可以在变量后面标注,也可以根据初始值自动推断,但如果声明变量时不赋初始值,则必须添加类型标注,否则在使用时会报错(即自动推断该变量为 undefined 的类型,因此不能赋其他值)。

常用的基础类型包括:

1
2
3
let isDone: boolean = false;
let clickCnt: number = 6;
let greeting: string = "Hello!";

原始值

  1. 布尔类型(boolean):

    1
    let sast_yyds: boolean = true;
    
  2. null 类型:

    1
    let null_value: null = null;
    
  3. undefined 类型:

    1
    let undefined_value: undefined = undefined;
    
  4. 数字类型(number):

    1
    let sast_rank: number = 1;
    
  5. bigint 类型(ES 2020 新增):

    1
    let a: bigint = 9007199254740991n;
    
  6. 字符串类型(string):

    1
    let course: string = 'TypeScript introduction';
    
  7. 符号类型(symbol,ES 6 新增):

    1
    let sym: symbol = Symbol('SAST');
    
  8. 尽管 TypeScript 会尝试阻止你这么做,但 JavaScript 是动态类型语言,你可以将一个变量赋为其他类型值。

  9. 尽管 TypeScript 更加会尝试阻止你,但 JavaScript 是弱类型语言,要小心随处可能存在的隐式类型转换,这可能会引起很难理解的行为,关于这一现象的详细解释可以参考 JavaScript 的变量与变量类型中的“JavaScript 的魔法”一节。

    1
    2
    console.log(1 + null);      // => 1
    console.log(1 + undefined); // => NaN
    

类型标注

简单类型标注

上文已经提到原始值的类型标注方式,但需要注意的是 TypeScript 允许使用字面量作为类型标注,如:

1
2
let one: 1 = 1;
one = 2;   // Error!

这里变量 one 的类型被限定为字面量 1 而不是所有的 number,这种标注的作用在下面会展示。

当你不知道该标记什么类型,或者你希望可以写任何类型时,可以谨慎使用 any,编译器将不会尝试对 any 类型的变量做任何的分析。

1
2
3
4
5
let a: any = 'whatever you like';
a = 12345; // OK

let notSure: any = 1;
notSure = "Maybe it is a string"; // OK

注意可能的 any 类型滥用

any 类型是目前 TypeScript 语言之中具有较大争议的一个设计,因为理论上我们可以将所有的变量声明为 any 从而绕过类型检查,这个时候 TypeScript 实际上退化为 JavaScript。

但是考虑到目前 Web 前端项目会引用大量的第三方库,开发者很多时候无法完全把握某些变量的信息,所以 any 类型是必要的。不过我们需要注意其使用,对于能够给定类型的变量则尽量不标记为 any

针对这一点,部分代码检查工具已经实装了检查“可疑 any”的功能。

类型别名

可以使用 type 关键词定义类型别名,在需要实现复杂的(尤其是嵌套的)类型时非常有用:

1
2
3
type NumberOne = 1;
let one: NumberOne = 1;
one = 2; // Error!

对象和数组的类型

对象和数组的类型应当这样标注:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Detailed object
let obj: {
    name: string,
    age: number,
    address: string
} = {
    name: "qiubao",
    age: 57,
    address: "Tsinghua"
};

let sast_summer: {
    frontend: string;
    backend: string;
} = {
    frontend: 'react',
    backend: 'gin',
};
1
2
3
4
let sast_summer_frontend: string[] = ['Java/Android', 'JS/TS/React'];

sast_summer.frontend = 'Java/Android'; // success
sast_summer.frontend = ['Java', 'JS']; // Error! 不能将 string[] 值分配给 string 类型的变量

对于固定长度和类型的数组(这实际上更接近于其他语言中的元组),可以这样标注:

1
let arr: [number, boolean] = [1, false];

联合类型和类型收窄

TypeScript 支持将变量声明为若干个类型之中的一种,这一般称为联合类型(Union Type)。 声明联合类型的时候,多个类型之间使用 | 隔开:

1
2
let union: number | string = 7;
union = "string"; // OK

联合类型最常用的地方是标注函数参数,这样就允许了函数接受多种类型的参数:

1
2
3
4
5
6
const addPrefix = (x: number | string): void => {
    console.log("Prefix " + x);
};

addPrefix(1); // OK
addPrefix("String"); // OK

对于上面提到的错误,可以这样解决:

1
2
3
4
5
6
7
8
9
let sast_summer: {
    frontend: string | string[];
    backend: string;
} = {
    frontend: 'react',
    backend: 'gin',
};

sast_summer.frontend = ['Java', 'JS']; // Success

这里我们声明 frontend 可能有两种类型,即 stringstring[],所以我们可以将 frontend 任意赋值为其中的一种,但这就导致我们在使用这一值时,不能精确判断上面包含的方法,例如我们尝试执行:

1
sast_summer.frontend.match(/react/);

编译器会告诉我们类型“string[]”上不存在属性“match”,因此编译器无法确定这行代码是正确的,但如果我们的代码足以使编译器推断出具体类型,如:

1
2
3
4
5
if (typeof sast_summer.frontend === 'string') {
    console.log(sast_summer.frontend.match(/react/));
} else {
    // sast_summer.frontend as string[]
}

编译器就会明白在两个分支上分别是两种类型,并允许调用相应的方法,这种行为被称为类型收窄

联合类型实现枚举类型

上文提到字面量可以作为类型标注,那么使用联合类型,就可以实现枚举行为:

1
2
3
type TwoPowers = 1 | 2 | 4 | 8 | 16;
let a: TwoPowers = 2; // OK
let b: TwoPowers = 5; // Error!

类型断言

当你确信你比编译器拥有关于某一变量类型的更多知识时,可以使用类型断言告诉编译器服从你的断言:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let val: any = "A string";
let stringLen: number = (val as string).length;

let sast_summer: {
    frontend: string | string[];
    backend: string;
} = {
    frontend: 'react',
    backend: 'gin',
};

console.log((sast_summer.frontend as string).match(/react/));

其中 as 关键词表示类型断言,但你不能断言一个变量为明显冲突的类型,如:

1
console.log(sast_summer.frontend as number);  // Error!

除非你有绝对的信心时,可以先将类型断言为 unknown 再断言为任意其他类型:

1
console.log(sast_summer.frontend as unknown as number);  // OK

never 类型与耗尽检查

TypeScript 支持一种特殊的类型,即 never 类型。这种类型常被用于标注函数返回值,代表这个函数永远不会终结或者会抛出异常:

1
2
3
const neverEnd = (): never => {
    while (true) {}
};

这种类型的值永远不能被实例化,也即尝试声明和使用 never 类型的值将会总是出现错误,利用个特点,我们可以检测程序是否考虑了所有的情况,这被称为耗尽检查(Exhaustive check)。

例如我们需要编写函数处理联合类型 number | string | boolean,其代码可能为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
type All = number | string | boolean;
// 关于函数表达式的更多细节将在稍后讲解
// switch 语句用法与 C/C++ 一致
const handler = (value: All) => {
    switch (typeof value) {
        case 'number':
            // do something
            break;
        case 'string':
            // do something
            break;
        case 'boolean':
            // do something
            break;
        default:
            let exhaustiveCheck: never = value;
    }
}

根据 never 的特点,default 分支的代码执行必然会产生错误,因此如果该 switch 语句未能穷尽 typeof value 的可能取值(而使得代码落入 default 分支),就会导致编译器报错。

如果修改类型 Allnumber | string | boolean | undefined,编译器会告诉我们不能将类型“undefined”分配给类型“never”,这就是因为当 value === undefined 时,会尝试将 undefined 赋给 never 类型的变量。 这样,handler 函数就会因为没有耗尽所有可能而报错,提醒开发者需要更新这个函数。 这种处理方式类似于 C++ 程序在不可能的分支上加入 assert(false) 进行检测。

评论

作者: abmfy