前言
在 JavaScript 中,变量是没有类型的,变量的值的类型是在运行时确定的,这被称为动态类型。
这意味着可以在不同的时间将不同类型的值赋给同一个变量,并且 JavaScript 会在运行时根据当前赋给变量的值来确定其类型。
示例:
let a; // 声明一个变量 a
a = 10; // 此时 a 的类型被确定为数字类型
a = "hello JavaScript"; // 现在 a 的类型变为字符串类型
a
的类型随着每次赋值而改变。
JavaScript 会在运行时根据实际赋给变量的值来自动调整和处理其类型。这种动态类型的特性使得 JavaScript 非常灵活,但也可能导致一些难以察觉的错误,因为类型的变化可能会在不经意间发生。
例如,想要一个变量是数字并进行数学运算,但它在运行时被意外地赋值为字符串,就会出错。
let a = 10;
let b = 5;
console.log(a + b); // 15
// 假如经历了一系列逻辑操作,需要重新对 b 赋值 并计算结果
b = "5";
console.log(a + b); // "105",假如在别的地方要使用这个结果,会导致难以排查的错误。
JavaScript是动态类型语言,对类型的检查和约束相对较弱。
TypeScript 是 JavaScript 的超集,它支持与JavaScript几乎相同的数据类型,在 JavaScript 的基础上增加了静态类型系统。这意味着在 TypeScript 中,开发者需要在编写代码时,可以选择添加类型注释来明确指定变量、函数参数、函数返回值等的类型。
注意:TypeScript 的类型注释将始终位于被输入的内容之后。
基础类型
string
:字符串类型。
// 声明一个变量 str,同时指定 str 的类型为 string
let str: string;
// str 的类型设置为 string,从此以后,str 的值只能是 字符串类型
str = "Hello"; // 字符串类型
str = 123; // 编译错误: Type 'number' is not assignable to type 'string'.
在编写代码时,编辑器报错:不能将类型“number”分配给类型“string”。
因为变量str
的类型是string
。
可以使用模版字符串:
let name: string = `张三`;
let desc: string = `我的名字是 ${name}`;
number
:数字类型。
数字类型不分int
和 float
,全是number
。
let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
let binaryLiteral: number = 0b1010;
let octalLiteral: number = 0o744;
除了支持十进制和十六进制字面量,TypeScript还支持ECMAScript 2015中引入的二进制和八进制字面量。
boolean
:布尔类型。
布尔类型只有两个值true
/ false
。
let isDone: boolean = false;
始终使用string
、number
或boolean
来表示类型。
数组类型
TypeScript有两种方式可以定义数组。
- 在基础数据类型后面接上
[]
,表示由此类型元素组成的一个数组:
let list1: number[] = [1, 2, 3]; // 数字数组,数组的每个元素都是number类型
let list2: string[] = ["list1", "list2"]; // 字符串数组,数组的每个元素都是string类型
let list3: boolean[] = [true, false, true]; // 布尔数组,数组的每个元素都是boolean类型let arr: string[][] = [["a", "b"], ["c", "d"]]; // 声明一个二维字符串数组。
- 使用数组泛型,
Array<元素类型>
:
let list1: Array<number> = [1, 2, 3];
let list2: Array<string> = ["list1", "list2"];
let list3: Array<boolean> = [true, false, true];
元组 Tuple
TypeScript 中的元组(Tuple)是一种特殊的数组类型。
元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。
特点:
- 固定的元素数量和类型顺序
- 元组中的元素数量是明确的,并且每个位置的元素类型是固定的。
- 混合类型
- 可以在一个元组中包含不同类型的元素。
示例:
// 定义一个包含字符串、数字和布尔值的元组
let myTuple: [string, number] = ["Hello", 42];// 访问元组中的元素,通过索引
console.log(myTuple[0]); // "Hello"
console.log(myTuple[1]); // 42
// 错误:Type 'string' is not assignable to type 'number'.(不能将类型“string”分配给类型“number”。)
myTuple[1] = "not a number"; // 编译错误:Type 'string' is not assignable to type 'number'.// 错误:Type 'true' is not assignable to type 'undefined'.(不能将类型“true”分配给类型“undefined”。)
// 错误:Tuple type '[string, number]' of length '2' has no element at index '2'.(长度为 "2" 的元组类型 "[string, number]" 在索引 "2" 处没有元素。)
myTuple[2] = true; // 错误:Argument of type 'boolean' is not assignable to parameter of type 'string | number'.(类型“boolean”的参数不能赋给类型“string | number”的参数。)
myTuple.push(true);// 语句没有报错,why?
// myTuple.push("world");
TypeScript 的元组和数组的区别:
- 元组:具有固定数量的元素,且每个元素的类型是明确且固定的。元组对每个位置的元素类型有严格的约束。
- 数组:元素数量可以动态变化。如果指定了类型,如
number[]
,则要求所有元素都是指定的类型;如果未指定类型,元素类型可以不同(any
类型)。
枚举
枚举(Enum
)类型是对Java Script标准数据类型的一个补充。
枚举(Enum
)是一种为一组数值赋予有意义名称的方式。
默认情况下,从 0
开始为元素编号。
enum Color {Red, Green, Blue}
console.log(Color);
// 输出以下结果
// {
// "0": "Red",
// "1": "Green",
// "2": "Blue",
// "Red": 0,
// "Green": 1,
// "Blue": 2
// }
代码定义了一个名为 Color
的枚举类型,其中 Red
被初始化为 0
,Green
为 1
,Blue
为 2
。
也可以手动为枚举成员赋值:
enum Color1 {Red = 11,Green = 22,Blue = 33
}
console.log(Color1);
// 输出
// {
// "11": "Red",
// "22": "Green",
// "33": "Blue",
// "Red": 11,
// "Green": 22,
// "Blue": 33
// }
枚举成员可以通过两种方式访问:
let c: Color = Color.Red; // 通过枚举名.成员名
console.log(c); // 输出 0(默认情况)let colorName = Color[2]; // 通过枚举值获取枚举名
console.log(colorName); // 输出 "Green"(默认情况)
Any
当一个变量被声明为 any
类型时,它可以被赋予任何类型的值,并且在对其进行操作时,TypeScript 编译器不会进行类型检查。
any
完全放弃了类型检查。
// 显式声明变量为any类型
let variable: any = 5;
variable = "Hello";
variable = true; let list: any[] = [1, true, "free"]; // list 包含了不同的类型的数据
list[1] = 66;// 隐式声明变量为any类型
// 声明变量 并且 不指定类型, TS 解析器会自动判断变量的类型为any
let variable1;
variable1 = "Hello";
variable1 = true;
在上述代码中,variable
、variable1
可以被随意赋值为不同类型的值。
any
类型的变量还可以赋值给任意类型的变量:
let variable: any = 5;
variable = "Hello";
variable = true; let str : string;
str = variable;
console.log(str); // true; TS类型检查器不会再检查 变量 str 。
通常,只有在确实无法确定变量类型或者需要与旧的 JavaScript 代码进行交互且类型难以明确时,才建议使用 any
类型。
unknown
unknown
类型表示一个值的类型是未知的。
只有 any
类型和 unknown
类型的值可以赋给 unknown
类型的变量。
unknown
类型是一种比 any
更安全的类型,对 unknown
类型的值进行操作是受到限制的。
例如,如果有一个变量 x
的类型是 unknown
,在对其进行操作之前,必须先进行类型断言或类型缩小的检查:
let x: unknown;
// 错误,不能直接对 unknown 类型的值进行操作
// x.toUpperCase(); // 报错:x 的类型是 “未知”
if (typeof x === "string") {// 经过类型缩小检查后,可以进行操作x.toUpperCase();
}
可以对 x
进行任意赋值:
let variable: any;
let x: unknown;
x = "Hello"; // 合法,因为可以将 any 或 unknown 类型的值赋给 unknown 类型的变量
x = true;
x = variable; // 合法,因为可以将 any 或 unknown 类型的值赋给 unknown 类型的变量
不能将 unknown
类型的变量 赋值给 除any
类型外的 其他类型的变量:
let str : string;
str = x; // 不能将类型“unknown”分配给类型“string”。let variable: any;
variable = x; // 合法,因为 any 类型可以被赋值任意类型的值
unknown
类型的主要用途是在处理可能来自不可信或动态来源的数据时,提供一种更严格的类型控制,以防止意外的类型错误。
unknown 和 any 的区别
- 类型安全性:
any
完全放弃了类型检查,可以对any
类型的变量执行任何操作,而 TypeScript 编译器不会产生错误。unknown
则更安全,对unknown
类型的变量进行操作时,TypeScript 编译器会要求先进行类型断言或类型缩小检查,以确保操作的合法性。
- 可赋值性:
- 任何类型的值都可以赋给
any
类型的变量。
any
类型的变量可以被赋值给任意类型的变量,并关闭该变量的TypeScript 静态类型检查。 - 只有
any
类型和unknown
类型的值可以赋给unknown
类型的变量。
unknown
类型的变量不可以被赋值给 除any
类型外 的 其他类型的变量。
- 任何类型的值都可以赋给
- 操作限制:
- 对于
any
类型的变量,可以直接调用任何方法和访问任何属性,而无需考虑其实际类型。 - 对于
unknown
类型的变量,在没有进行类型断言或缩小检查之前,不能直接调用方法或访问属性。
- 对于
Void
在 TypeScript 中,void
类型用于表示没有任何返回值的函数。
当变量被声明为 void
类型时,只能被赋值为 undefined
:
let unusable: void = undefined;
let unusable1: void = null; // 报错:不能将类型“null”分配给类型“void”。
void
类型用来表示函数没有返回值的情况。当你尝试将 null
赋值给一个 void
类型的变量时,编译器会报错,因为 null
表示对象的引用,而 void
不是一个对象,它只是一个表示无返回值的特殊类型。
当一个函数没有返回值时,你通常会见到其返回值类型是 void
:
function printMsg(): void {console.log("This is my message");
}function noReturn(): void {return // return undefined
}
Null 和 Undefined
在 TypeScript 中,null
的类型是null
, undefined
的类型是undefined
。
默认情况下null
和undefined
是所有类型的子类型。 就是说你可以把 null
和undefined
赋值给其他类型的变量。
let u: undefined = undefined; // 变量类型是undefined,值是undefined
let n: null = null; // 变量类型是null ,值是null let num: number = null; // 合法,但不推荐
let str: string;
console.log(str); // 输出 undefined
在 TypeScript 的严格空值检查模式(通过在 tsconfig.json
中设置 "strictNullChecks": true
)下,对 null
和 undefined
的处理更加严格。例如,函数参数不能默认是 null
或 undefined
,除非明确指定类型为 number | null
或 string | undefined
这样的联合类型。
文中所有例子都假设"strictNullChecks": false
。
Never
never
类型表示的是那些永不存在的值的类型。
在 TypeScript 中,一般不会直接创建 never
类型的变量。因为 never
表示永远不会有值的类型。
但在某些复杂的类型推导或函数的返回类型中,可能会出现被推断为 never
类型的情况。
它通常用于以下几种情况:
- 函数中抛出异常或进入一个无限循环:
// 返回never的函数必须存在无法达到的终点
function error(message: string): never {throw new Error(message);
}
// 返回never的函数必须存在无法达到的终点
function infiniteLoop(): never {while (true) {}
}// 推断的返回值类型为never
function fail() {return Error("Something failed");
}
- 用来表示一个函数的返回值类型,当这个函数永远不会正常返回(即总是以抛出异常结束)。
例如,如果一个函数用于处理所有可能的情况,并且对于某些输入它应该抛出错误,那么其返回类型可以是 never
:
function exhaustiveCheck(x: string): never {if (x === 'a') {// 一些处理逻辑} else if (x === 'b') {// 一些处理逻辑} else {throw new Error('Unexpected value');}
}
object
在 TypeScript 中,object
类型用于表示非原始类型,除 number
,string
,boolean
,symbol
,null
或undefined
外的类型。
let obj: object;
obj = { name: "张三" }; // obj 是一个对象
obj = function() {}; // obj是一个函数,函数也是对象let arr: object = [1, 2, 3]; // 数组是特殊的对象
在JavaScript中,几乎所有的数据类型都可以被视为对象,或者具有对象的某些特性。
基本数据类型如数字、字符串、布尔值,在 JavaScript 中都有对应的对象形式。像数字 5
对应的对象形式是 new Number(5)
,字符串 "hello"
对应的对象形式是 new String("hello")
,布尔值 true
对应的对象形式是 new Boolean(true)
。
函数也是对象,它们可以具有属性和方法。
数组也是一种特殊的对象。
甚至一些内置的全局对象,如 Math
、 Date
等,也提供了各种功能和方法。
示例:
function myFunction() {console.log("Hello!");
}
myFunction.property = "Some value"; // 函数可以添加属性let num = 5;
let numObj = new Number(num); // 将数字转换为对象形式
在 TypeScript 中,直接声明一个变量的类型为 object
相对较少使用,并且通常不是最佳实践。
let obj: object;
obj = { name: "张三" }; // 合法,TypeScript 无法对其属性进行具体的类型检查和智能提示。
console.log(obj.name); // Error: 类型“object”上不存在属性“name”。obj = { age: 18 }; // 合法,TypeScript 无法对其属性进行具体的类型检查和智能提示。
console.log(obj.age); // Error: 类型“object”上不存在属性“age”。
let obj: object
,只是声明变量的类型为 object
,TypeScript 认为 obj
可以是任何类型的对象,不能明确 obj
有哪些属性以及属性的类型。
当访问 obj.name
时,TypeScript 会报错:类型“object”上不存在属性“name”。
因为 TypeScript 不知道这个对象是否真的具有名为 name
的属性。
object
类型通常不是用于简单地判断一个变量是不是一个普通的对象,而是更侧重于描述对象所具有的特定属性结构。
(大多数情况下,object
被用来限制对象中的属性,而不是限制变量的类型是不是一个对象。)
// 声明一个对象类型的变量 obj ,并且指定其属性 name 的类型为字符串
let obj: {name: string};
obj = {}; // Error: Property 'name' is missing in type '{}' but required in type '{ name: string; }'.
// (类型 "{}" 中缺少属性 "name",但类型 "{ name: string; }" 中需要该属性。)// 对象obj的赋值必须和obj声明的结构一模一样
obj = {name: "张三"};
obj = {name: "张三", age: 18}; // Error: Object literal may only specify known properties, and 'age' does not exist in type '{ name: string; }'.
// (对象字面量只能指定已知属性,并且“age”不在类型“{ name: string; }”中。)
属性名后面加上?
,表示是可选属性:
// 这里的 ? 表示 age 属性是可选的
let obj: { name: string, age?: number };
obj = {name: "张三"}; // 合法, 因为 age 是可选属性
obj = {name: "张三", age: 18}; // 合法
假设对象obj
必须要有name
属性,其余属性可有可无:
let obj: { name: string, [propName: string]:any };
obj = {name: "张三", age: 18}; // 合法
obj = {name: "张三", age: 18, sex: "male"}; // 合法// Error: Property 'name' is missing in type '{ age: number; }' but required in type '{ [propName: string]: any; name: string; }'.
obj = {age: 18}; // 不合法,因为没有 'name' 属性// Error: Type 'number' is not assignable to type 'string'.
obj = {name: 18}; // 因为 'name' 属性不是字符串类型
let obj
声明了一个变量obj
。{ name: string, [propName: string]:any }
定义了这个变量的类型:name: string
明确指定了obj
必须具有一个名为name
的属性,且这个属性的值必须是字符串类型。[propName: string]:any
这部分是一个索引签名。propName
是一个变量名,在这里只是表示任意属性的名称。string
表示属性名的类型是字符串。any
表示对应属性的值可以是任何类型。
这意味着除了必须存在的name
属性是字符串类型外,obj
还可以有任意数量的其他属性,这些属性的名称是字符串,并且值可以是任何类型。
使用object
类型,可以更好的表示像Object.create
这样的API。
declare function create(o: object | null): void;
declare
关键字用于声明存在一个在其他地方定义(可能是在外部模块、全局环境或其他源文件)的函数。
这段代码声明了一个名为 create
的函数,它接受一个参数 o
,o
的类型可以是一个对象或者 null
,并且这个函数没有返回值(void
)。
调用create
函数:
create({ key: 'value' });
create(null); create(5); // 错误,因为 5 不是对象也不是 null
create('string'); // 错误,因为 'string' 不是对象也不是 null
字面量类型声明
在 TypeScript 中,字面量是指直接在代码中表示固定值的表达式。
使用字面量进行类型声明,限制变量的值就是该字面量的值。
注意:直接使用字面量赋值,不能再次修改值!
let a: 10; // 直接使用字面量进行类型声明,限制 a 的值只能是 10
a = 11; // Error: Type '11' is not assignable to type '10'.
在 let a: 10;
中,这是将变量 a
的类型声明为具体的数字字面量 10
。
这意味着变量 a
只能被赋值为 10
,任何其他的值都会导致类型错误。
使用字面量赋值,如果有多个值,怎么处理?
let sex: "male" | "female"; // 限制sex的值只能是 male 或 female
sex = "male";
sex = "female";
sex = "hello"; // Type '"hello"' is not assignable to type '"male" | "female"'.
在 let sex: "male" | "female";
这个声明中,变量 sex
的类型被限定为联合类型,其值只能是字符串 "male"
或者 "female"
。
联合类型声明
- 基本用法
// 变量 value 的类型被指定为联合类型 string | number
let value: string | number; // 表示 value 可以是字符串或者数字。
value = "hello typescript"; // 合法
value = 123; // 合法
value = []; // 不合法
- 函数参数
// 参数 value 的类型被指定为联合类型 string | number
function printValue(value: string | number) {if (typeof value ==='string') {console.log(`It's a string: ${value}`);} else {console.log(`It's a number: ${value}`);}
}printValue("World"); // 合法
printValue(456); // 合法
- 与对象类型相结合
interface Person {name: string;age: number;
}interface Animal {species: string;
}let entity: Person | Animal;entity = { name: "张三", age: 30 }; // 合法
entity = { species: "Dog" }; // 合法
entity = { name: "张三", age: 30, species: "Dog" }; // 合法
声明了一个变量 entity
,其类型为 Person
或 Animal
的联合类型,这意味着 entity
可以被赋值为符合 Person
接口结构的对象,或者符合 Animal
接口结构的对象,也可以同时符合 Person
和 Animal
结构的对象。
通过联合类型的声明,使得 entity
变量能够灵活地接受不同类型但具有特定结构的对象值。
- 数组中的联合声明
let arr: (string | number)[];
arr = ["Hello", 123, "World", 456]; // 合法arr = ["Hello", 123, "World", true]; // 不合法
// Error: Type 'boolean' is not assignable to type 'string | number'.
(string | number)[]
是数组的类型声明。其中 string | number
是联合类型,表示数组中的元素可以是字符串类型(string
)或者数字类型(number
)。
联合类型为处理可能具有多种类型的值提供了灵活性。
交叉类型声明
在 TypeScript 中,交叉类型(Intersection Type)用于将多个类型合并为一个新的类型,该新类型具有所合并类型的所有成员。
交叉类型使用 &
符号来创建。
例如,如果有两个接口 User
和 Employee
:
interface User {name: string;age: number;sayYes(): void;
}interface Employee {jobTitle: string;salary: number;sayNo(): void;
}
创建一个交叉类型 UserEmployee
:
type UserEmployee = User & Employee;// 错误: “UserEmployee”仅表示类型,但在此处却作为值使用。
console.log(UserEmployee ); // Error: 'UserEmployee' only refers to a type, but is being used as a value here.
UserEmployee
类型的对象同时具有 User
和 Employee
接口中定义的所有属性和方法:
let person: UserEmployee = {name: "张三",age: 30,jobTitle: "Developer",salary: 10000,sayYes() {console.log('sayYes!');},sayNo() {console.log('sayNo!');}
};
在交叉类型中,需要注意以下几点:
- 同名属性的类型必须兼容。如果两个接口中都有一个名为 id 的属性,但一个是字符串类型,另一个是数字类型,就会导致类型错误。
- 方法也会被合并,如果两个接口中有同名的方法,那么它们的实现需要保持一致,否则也会导致类型错误。
- 交叉类型可以用于多个接口、类型别名甚至具体的对象类型的合并。
交叉类型在一些场景中非常有用,比如需要一个对象同时满足多个不同的类型约束,或者需要将多个相关但又有所不同的类型组合在一起。