TypeScript基础语法的核心概念解析
一、TypeScript 简介
TypeScript 是由微软开发的一款开源的编程语言,它是 JavaScript 的超集,这意味着任何合法的 JavaScript 代码都是合法的 TypeScript 代码。TypeScript 为 JavaScript 添加了静态类型系统,使得开发者可以在代码编写阶段就发现一些潜在的错误,而不是等到运行时才暴露问题,从而提高代码的可靠性和可维护性。
例如,在 JavaScript 中,以下代码在运行前不会有任何错误提示:
function add(a, b) {
return a + b;
}
const result = add('1', 2);
这里将字符串和数字相加,在运行时可能会导致不符合预期的结果,但在编写代码时却无法察觉。而在 TypeScript 中,我们可以通过类型标注来避免这类问题:
function add(a: number, b: number): number {
return a + b;
}
const result = add('1', 2);
// 这里 TypeScript 编译器会报错,提示类型不匹配
二、基本数据类型
2.1 布尔类型(boolean)
布尔类型是 TypeScript 中最基本的数据类型之一,它只有两个值:true
和 false
。在 TypeScript 中声明布尔类型变量如下:
let isDone: boolean = false;
这里通过 : boolean
明确标注了变量 isDone
的类型为布尔类型。如果我们尝试给 isDone
赋值其他类型的值,比如:
let isDone: boolean = 'false';
// 编译器会报错,提示不能将字符串类型赋值给布尔类型
2.2 数字类型(number)
在 TypeScript 中,所有的数字都是以 64 位浮点数的形式存储,和 JavaScript 一样。声明数字类型变量示例如下:
let age: number = 25;
let pi: number = 3.14;
同样,如果赋值类型不匹配,会报错:
let age: number = '25';
// 报错,不能将字符串赋值给数字类型
2.3 字符串类型(string)
字符串类型用于表示文本数据。在 TypeScript 中,可以使用单引号 '
或双引号 "
来定义字符串。
let name: string = 'John';
let greeting: string = "Hello, " + name;
模板字符串在 TypeScript 中也同样适用,它允许我们更方便地嵌入变量:
let name: string = 'John';
let greeting: string = `Hello, ${name}`;
2.4 空值(void)
void
类型表示没有任何类型。通常用于函数返回值类型,当一个函数没有返回值时,可以将其返回值类型标注为 void
。
function printMessage(): void {
console.log('This is a message');
}
如果尝试给 void
类型的变量赋值(除了 null
和 undefined
,在严格模式下 null
和 undefined
也不能赋值给 void
),会报错:
let myVoid: void = 10;
// 报错,不能将数字赋值给 void 类型
2.5 Null 和 Undefined
在 TypeScript 中,null
和 undefined
都有自己的类型,分别为 null
和 undefined
。它们通常用于表示值的缺失或未初始化状态。
let myNull: null = null;
let myUndefined: undefined = undefined;
在严格模式下,null
和 undefined
只能赋值给自身类型或者 void
类型。而在非严格模式下,它们可以赋值给其他类型。
2.6 任意类型(any)
any
类型表示可以是任意类型的值。当我们不确定一个变量的类型,或者希望它可以接受任何类型的值时,可以使用 any
类型。
let data: any = 'initial value';
data = 123;
data = true;
虽然 any
类型提供了很大的灵活性,但过度使用会失去 TypeScript 类型检查的优势,导致代码的可维护性降低。
2.7 联合类型(Union Types)
联合类型允许一个变量具有多种类型。通过 |
符号来定义联合类型。
let value: string | number;
value = 'hello';
value = 42;
在使用联合类型的变量时,我们只能访问联合类型中所有类型共有的属性和方法。例如:
function printValue(value: string | number) {
console.log(value.length);
// 报错,number 类型没有 length 属性
}
要正确处理联合类型,可以使用类型守卫,比如 typeof
操作符:
function printValue(value: string | number) {
if (typeof value ==='string') {
console.log(value.length);
} else {
console.log(value.toFixed(2));
}
}
2.8 类型断言(Type Assertion)
类型断言是告诉编译器,我们比它更了解某个值的类型,让编译器按照我们指定的类型来处理。类型断言有两种语法:
as
语法:
let someValue: any = 'this is a string';
let strLength: number = (someValue as string).length;
- 尖括号语法(在 JSX 中不能使用):
let someValue: any = 'this is a string';
let strLength: number = (<string>someValue).length;
三、数组和元组
3.1 数组
在 TypeScript 中定义数组有两种方式。一种是在类型后面加上 []
:
let numbers: number[] = [1, 2, 3];
另一种是使用泛型数组类型 Array<类型>
:
let numbers: Array<number> = [1, 2, 3];
如果数组中元素类型不一致,可以使用联合类型:
let mixedArray: (number | string)[] = [1, 'two', 3];
3.2 元组(Tuple)
元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。例如,定义一个包含字符串和数字的元组:
let user: [string, number] = ['John', 25];
如果访问元组中不存在的索引,会报错:
let user: [string, number] = ['John', 25];
console.log(user[2]);
// 报错,元组越界
如果给元组元素赋值类型不匹配的值,也会报错:
let user: [string, number] = ['John', 25];
user[0] = 123;
// 报错,不能将数字赋值给字符串类型
四、函数
4.1 函数定义与类型标注
在 TypeScript 中,函数参数和返回值都可以进行类型标注。
function add(a: number, b: number): number {
return a + b;
}
这里 a
和 b
参数标注为 number
类型,函数返回值标注为 number
类型。如果调用函数时参数类型不匹配,会报错:
function add(a: number, b: number): number {
return a + b;
}
const result = add('1', 2);
// 报错,不能将字符串作为 number 类型参数传递
4.2 可选参数和默认参数
可选参数在参数名后加上 ?
表示,并且必须放在参数列表的末尾。
function greet(name: string, message?: string) {
if (message) {
return `Hello, ${name}! ${message}`;
} else {
return `Hello, ${name}!`;
}
}
const greeting1 = greet('John');
const greeting2 = greet('John', 'How are you?');
默认参数可以在定义函数时直接给参数指定默认值。
function greet(name: string, message = 'How are you?') {
return `Hello, ${name}! ${message}`;
}
const greeting1 = greet('John');
const greeting2 = greet('John', 'Good day!');
4.3 剩余参数
剩余参数允许我们将不确定数量的参数作为一个数组来处理。在参数名前加上 ...
表示剩余参数。
function sum(...numbers: number[]): number {
return numbers.reduce((acc, num) => acc + num, 0);
}
const result = sum(1, 2, 3, 4);
4.4 函数类型
可以将函数类型赋值给变量,或者作为其他函数的参数类型。
let add: (a: number, b: number) => number;
add = function(a: number, b: number): number {
return a + b;
};
这里定义了一个函数类型 (a: number, b: number) => number
,表示接受两个 number
类型参数并返回一个 number
类型值的函数。
五、接口(Interface)
5.1 接口定义与使用
接口是 TypeScript 中非常重要的概念,它用于定义对象的形状(结构)。接口可以描述对象中属性的类型和是否可选。
interface User {
name: string;
age: number;
email?: string;
}
function printUser(user: User) {
console.log(`Name: ${user.name}, Age: ${user.age}`);
if (user.email) {
console.log(`Email: ${user.email}`);
}
}
let myUser: User = {
name: 'John',
age: 25,
email: 'john@example.com'
};
printUser(myUser);
在上述代码中,User
接口定义了 name
为字符串类型,age
为数字类型,email
为可选的字符串类型。
5.2 接口的继承
接口可以继承其他接口,从而扩展其功能。
interface Person {
name: string;
age: number;
}
interface Employee extends Person {
employeeId: number;
}
let myEmployee: Employee = {
name: 'Jane',
age: 30,
employeeId: 1001
};
这里 Employee
接口继承了 Person
接口,所以 Employee
接口不仅拥有 name
和 age
属性,还新增了 employeeId
属性。
5.3 函数类型接口
接口也可以用于定义函数类型。
interface AddFunction {
(a: number, b: number): number;
}
let add: AddFunction = function(a: number, b: number): number {
return a + b;
};
这里 AddFunction
接口定义了一个接受两个 number
类型参数并返回 number
类型值的函数类型。
六、类型别名(Type Alias)
类型别名是给一个类型起一个新的名字。它可以用于基本类型、联合类型、函数类型等。
type MyNumber = number;
let num: MyNumber = 10;
type StringOrNumber = string | number;
let value: StringOrNumber = 'hello';
value = 42;
type AddFunction = (a: number, b: number) => number;
let add: AddFunction = function(a: number, b: number): number {
return a + b;
};
类型别名和接口有一些相似之处,但也有区别。接口只能用于定义对象的形状,而类型别名可以用于任何类型。并且类型别名不能被继承,而接口可以。
七、类(Class)
7.1 类的定义与实例化
在 TypeScript 中,类是面向对象编程的基础。类可以包含属性、方法、构造函数等。
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound`);
}
}
let dog = new Animal('Buddy');
dog.speak();
在上述代码中,Animal
类有一个 name
属性,构造函数用于初始化 name
属性,speak
方法用于输出动物发出声音的信息。
7.2 访问修饰符
TypeScript 支持三种访问修饰符:public
(默认)、private
和 protected
。
public
:可以在类内部和外部访问。
class Person {
public name: string;
constructor(name: string) {
this.name = name;
}
}
let person = new Person('John');
console.log(person.name);
private
:只能在类内部访问。
class Person {
private name: string;
constructor(name: string) {
this.name = name;
}
}
let person = new Person('John');
console.log(person.name);
// 报错,无法在类外部访问 private 属性
protected
:只能在类内部和子类中访问。
class Animal {
protected name: string;
constructor(name: string) {
this.name = name;
}
}
class Dog extends Animal {
bark() {
console.log(`${this.name} barks`);
}
}
let dog = new Dog('Buddy');
console.log(dog.name);
// 报错,无法在类外部访问 protected 属性
dog.bark();
7.3 类的继承
类可以继承其他类,通过 extends
关键字。子类可以重写父类的方法。
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound`);
}
}
class Dog extends Animal {
speak() {
console.log(`${this.name} barks`);
}
}
let dog = new Dog('Buddy');
dog.speak();
这里 Dog
类继承了 Animal
类,并重写了 speak
方法。
7.4 抽象类
抽象类是一种不能被实例化的类,它主要用于为其他类提供一个通用的基类。抽象类可以包含抽象方法,抽象方法没有具体的实现,必须在子类中实现。
abstract class Shape {
abstract getArea(): number;
}
class Circle extends Shape {
radius: number;
constructor(radius: number) {
super();
this.radius = radius;
}
getArea(): number {
return Math.PI * this.radius * this.radius;
}
}
let circle = new Circle(5);
console.log(circle.getArea());
在上述代码中,Shape
是抽象类,它有一个抽象方法 getArea
。Circle
类继承自 Shape
类,并实现了 getArea
方法。
八、泛型(Generics)
8.1 泛型函数
泛型允许我们在定义函数、接口或类时使用类型参数,使得代码可以复用,同时保持类型安全。
function identity<T>(arg: T): T {
return arg;
}
let result1 = identity<number>(10);
let result2 = identity<string>('hello');
在上述代码中,<T>
是类型参数,T
可以代表任何类型。在调用 identity
函数时,可以显式指定类型参数,也可以让编译器根据传入的参数类型自动推断。
8.2 泛型接口
泛型也可以用于接口。
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
这里定义了一个泛型接口 GenericIdentityFn
,它接受一个类型参数 T
,并定义了一个函数类型,该函数接受一个 T
类型参数并返回 T
类型值。
8.3 泛型类
泛型同样适用于类。
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) {
return x + y;
};
在这个例子中,GenericNumber
类使用了泛型 T
,使得它可以适用于不同类型的数据。
九、枚举(Enum)
枚举是一种用于定义一组命名常量的类型。TypeScript 支持数字枚举和字符串枚举。
9.1 数字枚举
enum Direction {
Up = 1,
Down,
Left,
Right
}
let myDirection = Direction.Up;
console.log(myDirection);
在上述代码中,Direction
是一个数字枚举,Up
初始化为 1,其他成员会自动递增。
9.2 字符串枚举
enum Status {
Success = 'success',
Failure = 'failure'
}
let myStatus = Status.Success;
console.log(myStatus);
字符串枚举的成员值必须是字符串字面量。
9.3 异构枚举(不推荐使用)
异构枚举是指枚举成员既有数字类型又有字符串类型,虽然 TypeScript 支持,但不推荐使用,因为它可能会导致一些混淆。
enum MixedEnum {
First = 'hello',
Second = 42
}
十、类型推断
TypeScript 具有强大的类型推断能力,它可以根据代码的上下文自动推断出变量的类型。
let num = 10;
// TypeScript 推断 num 为 number 类型
let str = 'hello';
// TypeScript 推断 str 为 string 类型
function add(a, b) {
return a + b;
}
let result = add(1, 2);
// TypeScript 推断 add 函数的参数和返回值类型
在大多数情况下,类型推断可以减少我们显式标注类型的工作量,但在一些复杂的情况下,还是需要显式标注类型以确保代码的正确性和可读性。
十一、类型兼容性
TypeScript 的类型兼容性是基于结构类型系统的。结构类型系统是指类型的兼容性是由它们的结构决定的,而不是由它们的声明决定的。
interface A {
x: number;
}
interface B {
x: number;
y: number;
}
let a: A = { x: 10 };
let b: B = { x: 10, y: 20 };
a = b;
// 可以将 B 类型赋值给 A 类型,因为 B 包含了 A 的所有属性
b = a;
// 报错,A 类型缺少 B 类型的 y 属性
在函数类型的兼容性方面,参数类型是逆变的,返回值类型是协变的。
let func1: (a: number) => void;
let func2: (a: string) => void;
func1 = func2;
// 报错,参数类型不兼容,string 不能赋值给 number
func2 = func1;
// 可以,因为函数参数类型是逆变的,number 可以赋值给 string 的超类型 any
十二、装饰器(Decorator)
装饰器是一种特殊的声明,可以附加到类声明、方法、访问器、属性或参数上。它本质上是一个函数,在运行时会被调用。
12.1 类装饰器
function logClass(target: any) {
console.log('This is a class decorator');
console.log(target);
}
@logClass
class MyClass {}
在上述代码中,logClass
是一个类装饰器,当 MyClass
类被定义时,logClass
函数会被调用,参数 target
就是 MyClass
类的构造函数。
12.2 方法装饰器
function logMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log('This is a method decorator');
console.log(target);
console.log(propertyKey);
console.log(descriptor);
}
class MyClass {
@logMethod
myMethod() {}
}
方法装饰器接受三个参数:target
是类的原型对象,propertyKey
是方法名,descriptor
是方法的属性描述符。
12.3 属性装饰器
function logProperty(target: any, propertyKey: string) {
console.log('This is a property decorator');
console.log(target);
console.log(propertyKey);
}
class MyClass {
@logProperty
myProperty: string;
}
属性装饰器接受两个参数:target
是类的原型对象,propertyKey
是属性名。
装饰器在实际开发中常用于日志记录、权限验证、依赖注入等场景。但需要注意的是,装饰器目前还处于实验阶段,在不同的环境中可能有不同的行为。
通过对以上 TypeScript 基础语法核心概念的解析,相信你对 TypeScript 有了更深入的理解,能够更好地运用 TypeScript 进行前端开发,编写出更健壮、可维护的代码。在实际项目中,还需要不断实践和积累经验,充分发挥 TypeScript 的优势。