Typescript中的数据类型
基础数据类型
在 TypeScript 中,基础数据类型是构成复杂数据结构的基石。这些数据类型直接由语言提供,开发人员可以在代码中直接使用它们来存储简单的值。
布尔类型(boolean)
布尔类型只有两个取值:true
和 false
。它常用于逻辑判断,比如在条件语句 if
中,用来决定代码的执行路径。
let isDone: boolean = false;
if (isDone) {
console.log('任务已完成');
} else {
console.log('任务未完成');
}
这里我们声明了一个名为 isDone
的布尔变量,并初始化为 false
。然后在 if - else
语句中,根据 isDone
的值输出不同的信息。
数字类型(number)
TypeScript 中的数字类型用于表示整数和浮点数。与 JavaScript 类似,TypeScript 没有专门的整数类型,所有数字都是以 64 位双精度浮点格式存储。
let myNumber: number = 42;
let pi: number = 3.14159;
let binaryNumber: number = 0b1010; // 二进制表示,对应十进制的10
let octalNumber: number = 0o755; // 八进制表示,对应十进制的493
let hexadecimalNumber: number = 0x1F; // 十六进制表示,对应十进制的31
以上代码展示了不同进制数字的表示方法,在 TypeScript 中都统一用 number
类型表示。
字符串类型(string)
字符串类型用于表示文本数据,它由一系列字符组成,用单引号 '
、双引号 "
或模板字符串 ``
` 包裹。
let myString1: string = 'Hello, world!';
let myString2: string = "这也是一个字符串";
let name: string = '张三';
let greeting: string = `你好,${name}`; // 模板字符串,可以嵌入变量
模板字符串允许我们在字符串中嵌入表达式,通过 ${}
语法,这在拼接复杂字符串时非常方便。
空值类型(void)
void
类型表示没有任何值,通常用于函数返回值类型。当一个函数不返回任何值时,它的返回值类型就是 void
。
function printMessage(): void {
console.log('这是一条消息');
}
let result: void = printMessage(); // result 只能被赋值为 undefined 或者没有值
这里定义了一个 printMessage
函数,它不返回任何值,所以返回值类型为 void
。变量 result
被声明为 void
类型,它只能被赋值为 undefined
(在严格模式下,void
类型的值只能是 undefined
) 或者不赋值。
null 和 undefined
在 TypeScript 中,null
和 undefined
都有自己的类型,分别为 null
和 undefined
。它们通常用来表示值的缺失或未初始化状态。
let myNull: null = null;
let myUndefined: undefined = undefined;
默认情况下,null
和 undefined
是所有类型的子类型,也就是说可以把 null
和 undefined
赋值给其他类型的变量。
let num: number = null; // 这样的赋值在默认情况下是允许的
let str: string = undefined; // 同样,这样的赋值在默认情况下也被允许
但是,当开启 strictNullChecks
编译选项时,null
和 undefined
只能赋值给 void
和它们各自对应的类型。
// 开启 strictNullChecks 后
let num: number;
num = null; // 报错:不能将类型“null”分配给类型“number”
never 类型
never
类型表示永远不会出现的值的类型。例如,一个抛出异常或根本不会有返回值的函数,其返回值类型就是 never
。
function throwError(message: string): never {
throw new Error(message);
}
function infiniteLoop(): never {
while (true) {
// 无限循环,永远不会结束
}
}
throwError
函数总是抛出异常,所以它的返回值类型是 never
。infiniteLoop
函数进入无限循环,永远不会返回,其返回值类型也是 never
。
复杂数据类型
除了基础数据类型,TypeScript 还提供了多种复杂数据类型,这些类型可以用来表示更结构化的数据。
数组类型
数组是一种有序的数据集合,可以存储多个相同类型的值。在 TypeScript 中有两种方式来定义数组类型。
类型 + 方括号表示法
let numbers: number[] = [1, 2, 3, 4, 5];
let strings: string[] = ['apple', 'banana', 'cherry'];
这里 numbers
是一个 number
类型的数组,strings
是一个 string
类型的数组。
泛型数组表示法
let anyArray: Array<number> = [1, 2, 3];
let anotherArray: Array<string> = ['a', 'b', 'c'];
Array<T>
是泛型表示法,其中 T
是数组元素的类型。这两种方式本质上是等价的,开发人员可以根据个人喜好选择使用。
元组类型
元组类型允许我们定义一个固定长度的数组,并且每个位置的元素类型可以不同。
let user: [string, number] = ['张三', 25];
let point: [number, number] = [10, 20];
在 user
元组中,第一个元素是 string
类型,第二个元素是 number
类型。元组类型非常有用,比如当你需要返回多个不同类型的值时,元组可以提供一种类型安全的方式。
访问元组元素时,需要按照定义的类型来访问。
let name: string = user[0];
let age: number = user[1];
如果访问超出元组长度的索引,TypeScript 会报错。
枚举类型
枚举类型是一种为一组命名常量提供更友好方式的类型。它允许我们定义一个命名的常量集合。
数字枚举
enum Color {
Red,
Green,
Blue
}
let myColor: Color = Color.Green;
console.log(myColor); // 输出 1
在数字枚举中,如果没有显式赋值,枚举成员会从 0
开始自动递增。这里 Color.Red
的值为 0
,Color.Green
的值为 1
,Color.Blue
的值为 2
。
字符串枚举
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT'
}
let myDirection: Direction = Direction.Right;
console.log(myDirection); // 输出 RIGHT
字符串枚举中的成员必须显式赋值,它们的值就是所赋的字符串。
联合类型
联合类型允许一个变量可以是多种类型中的一种。我们使用 |
来分隔不同的类型。
let value: string | number;
value = 'hello';
value = 42;
这里 value
变量可以是 string
类型,也可以是 number
类型。在使用联合类型的变量时,需要注意只能访问这些类型共有的属性和方法。
function printValue(v: string | number) {
// 这里只能访问 string 和 number 共有的属性和方法,比如 toString()
console.log(v.toString());
}
printValue('abc');
printValue(123);
交叉类型
交叉类型是将多个类型合并为一个类型。它包含了所有类型的特性,使用 &
来表示。
interface A {
a: string;
}
interface B {
b: number;
}
let ab: A & B = { a: 'hello', b: 42 };
这里 ab
变量的类型是 A & B
,它必须同时满足 A
接口和 B
接口的要求,即同时具有 a
属性(类型为 string
)和 b
属性(类型为 number
)。
类型别名与接口
在 TypeScript 中,类型别名和接口是用于定义自定义类型的两种重要方式。
类型别名
类型别名可以为任意类型定义一个新的名字,提高代码的可读性和可维护性。
type UserId = number | string;
let userId: UserId = 123;
userId = '456';
这里定义了一个 UserId
类型别名,它可以是 number
或 string
类型。然后我们声明了一个 userId
变量,它可以被赋值为 number
或 string
类型的值。
类型别名也可以用于更复杂的类型,比如函数类型。
type AddFunction = (a: number, b: number) => number;
let add: AddFunction = function (x, y) {
return x + y;
};
这里 AddFunction
是一个函数类型别名,它表示一个接受两个 number
类型参数并返回一个 number
类型值的函数。
接口
接口主要用于定义对象的形状,即对象所具有的属性和方法。
interface User {
name: string;
age: number;
email: string;
}
let user: User = {
name: '李四',
age: 30,
email: 'lisi@example.com'
};
在上述代码中,User
接口定义了一个对象应该具有 name
(string
类型)、age
(number
类型)和 email
(string
类型)属性。user
变量的类型为 User
,它必须满足 User
接口的定义。
接口还可以定义可选属性和只读属性。
可选属性
interface Product {
name: string;
price: number;
description?: string; // 可选属性
}
let product: Product = {
name: '手机',
price: 5999
};
这里 description
是一个可选属性,在创建 Product
类型的对象时,可以不提供该属性。
只读属性
interface Point {
readonly x: number;
readonly y: number;
}
let point: Point = { x: 10, y: 20 };
// point.x = 20; // 报错:无法分配到 "x" ,因为它是只读属性
x
和 y
是只读属性,一旦对象被创建,这些属性的值就不能被修改。
函数类型
在 TypeScript 中,函数也是一种类型。我们可以为函数定义输入参数的类型和返回值的类型。
函数声明
function add(a: number, b: number): number {
return a + b;
}
这里 add
函数接受两个 number
类型的参数 a
和 b
,并返回一个 number
类型的值。
函数表达式
let subtract: (a: number, b: number) => number = function (x, y) {
return x - y;
};
这是一个函数表达式,subtract
变量的类型是一个函数类型,它接受两个 number
类型参数并返回一个 number
类型值。
可选参数和默认参数
函数参数可以是可选的,通过在参数名后加 ?
来表示。
function greet(name: string, message?: string) {
if (message) {
console.log(`${name}, ${message}`);
} else {
console.log(`Hello, ${name}`);
}
}
greet('王五');
greet('赵六', '最近怎么样?');
这里 message
是一个可选参数,调用 greet
函数时可以不提供该参数。
我们还可以为参数提供默认值。
function multiply(a: number, b: number = 1) {
return a * b;
}
console.log(multiply(5)); // 输出 5
console.log(multiply(5, 3)); // 输出 15
b
参数有默认值 1
,如果调用 multiply
函数时不提供 b
参数的值,它会使用默认值。
剩余参数
剩余参数允许我们将多个参数收集到一个数组中。
function sum(...numbers: number[]) {
return numbers.reduce((acc, num) => acc + num, 0);
}
console.log(sum(1, 2, 3)); // 输出 6
console.log(sum(4, 5, 6, 7)); // 输出 22
...numbers
是剩余参数,它将所有传入的参数收集到一个 number
类型的数组中,然后我们可以对这个数组进行操作。
类型推断
TypeScript 具有强大的类型推断能力,它可以在很多情况下自动推断出变量的类型,而无需我们显式地声明类型。
基础类型推断
let num = 42; // TypeScript 自动推断 num 为 number 类型
let str = 'hello'; // 自动推断 str 为 string 类型
这里 num
和 str
没有显式声明类型,但 TypeScript 根据初始化值推断出了它们的类型。
函数返回值推断
function add(a, b) {
return a + b;
}
let result = add(1, 2); // result 被推断为 number 类型
在 add
函数中,虽然没有显式声明参数和返回值类型,但 TypeScript 根据函数体的逻辑推断出返回值类型为 number
,因此 result
变量被推断为 number
类型。
上下文类型推断
上下文类型推断是指 TypeScript 根据变量使用的上下文来推断其类型。
document.addEventListener('click', function (event) {
console.log(event.type); // event 被推断为 MouseEvent 类型
});
这里 addEventListener
的第二个参数是一个函数,根据上下文,TypeScript 推断出 event
的类型为 MouseEvent
,因为 click
事件的回调函数参数通常是 MouseEvent
类型。
类型兼容性
在 TypeScript 中,类型兼容性用于判断一个类型是否可以赋值给另一个类型。
基本规则
TypeScript 的类型兼容性基于结构类型系统。在结构类型系统中,如果 A
类型的结构包含了 B
类型的结构,那么 B
类型可以赋值给 A
类型。
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
let animal: Animal;
let dog: Dog = { name: 'Buddy', breed: 'Golden Retriever' };
animal = dog; // 可以赋值,因为 Dog 包含了 Animal 的结构
这里 Dog
接口扩展自 Animal
接口,Dog
类型的对象可以赋值给 Animal
类型的变量,因为 Dog
包含了 Animal
的所有属性。
函数参数的类型兼容性
对于函数参数的类型兼容性,是逆变的。也就是说,如果 A
类型的函数参数可以赋值给 B
类型的函数参数,那么 B
类型的函数可以赋值给 A
类型的函数。
let func1: (a: number) => void = function (num) { };
let func2: (a: any) => void = function (anyValue) { };
func1 = func2; // 可以赋值,因为 number 是 any 的子类型
这里 func2
的参数类型 any
比 func1
的参数类型 number
更宽泛,所以 func2
可以赋值给 func1
。
函数返回值的类型兼容性
函数返回值的类型兼容性是协变的。即如果 A
类型的函数返回值可以赋值给 B
类型的函数返回值,那么 A
类型的函数可以赋值给 B
类型的函数。
let func3: () => string = function () { return 'hello'; };
let func4: () => any = function () { return 42; };
func4 = func3; // 可以赋值,因为 string 是 any 的子类型
这里 func3
的返回值类型 string
是 func4
的返回值类型 any
的子类型,所以 func3
可以赋值给 func4
。
类型断言
类型断言是一种告诉编译器“相信我,我知道自己在做什么”的方式。当我们比编译器更清楚某个值的类型时,可以使用类型断言。
语法
有两种类型断言的语法:尖括号语法和 as
语法。
尖括号语法
let someValue: any = 'this is a string';
let strLength: number = (<string>someValue).length;
这里我们将 someValue
断言为 string
类型,然后访问其 length
属性。
as
语法
let someOtherValue: any = 'this is also a string';
let anotherStrLength: number = (someOtherValue as string).length;
这两种语法的效果是一样的,在 TypeScript 代码中使用 JSX 时,必须使用 as
语法进行类型断言。
类型断言的限制
类型断言并不是类型转换,它只是告诉编译器以某种类型来处理值,并不会在运行时改变值的实际类型。而且类型断言只能在兼容的类型之间进行,比如不能将一个 string
断言为 number
。
let value: string = 'abc';
// let num: number = value as number; // 报错:类型“string”的参数不能赋给类型“number”的参数
高级类型
除了前面介绍的基本和复杂类型,TypeScript 还提供了一些高级类型,用于处理更复杂的类型操作。
映射类型
映射类型允许我们基于一个已有的类型创建一个新类型,通过对已有类型的属性进行变换。
interface User {
name: string;
age: number;
email: string;
}
type ReadonlyUser = {
readonly [P in keyof User]: User[P];
};
let readonlyUser: ReadonlyUser = {
name: '张三',
age: 25,
email: 'zhangsan@example.com'
};
// readonlyUser.name = '李四'; // 报错:无法分配到 "name" ,因为它是只读属性
这里 ReadonlyUser
类型通过映射 User
类型的属性,并将每个属性变为只读属性。keyof User
获取 User
类型的所有属性名,P in keyof User
对每个属性名进行遍历,User[P]
获取属性的值类型。
条件类型
条件类型允许我们根据类型关系选择不同的类型。
type IsString<T> = T extends string? true : false;
type Result1 = IsString<string>; // true
type Result2 = IsString<number>; // false
IsString
是一个条件类型,它判断类型 T
是否为 string
类型,如果是则返回 true
,否则返回 false
。
索引类型
索引类型允许我们通过索引来访问对象类型的属性类型。
interface Product {
name: string;
price: number;
inStock: boolean;
}
type NameType = Product['name']; // string
type PriceType = Product['price']; // number
这里通过 Product['name']
获取 Product
类型中 name
属性的类型 string
,Product['price']
获取 price
属性的类型 number
。
通过对 TypeScript 中各种数据类型的深入了解,开发人员可以编写出更健壮、类型安全的代码,充分发挥 TypeScript 在大型项目中的优势。无论是基础数据类型的使用,还是复杂类型和高级类型的灵活运用,都有助于提高代码的可读性、可维护性和可靠性。