MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript基础语法的核心概念解析

2024-10-064.5k 阅读

一、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 中最基本的数据类型之一,它只有两个值:truefalse。在 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 类型的变量赋值(除了 nullundefined,在严格模式下 nullundefined 也不能赋值给 void),会报错:

let myVoid: void = 10; 
// 报错,不能将数字赋值给 void 类型

2.5 Null 和 Undefined

在 TypeScript 中,nullundefined 都有自己的类型,分别为 nullundefined。它们通常用于表示值的缺失或未初始化状态。

let myNull: null = null;
let myUndefined: undefined = undefined;

在严格模式下,nullundefined 只能赋值给自身类型或者 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)

类型断言是告诉编译器,我们比它更了解某个值的类型,让编译器按照我们指定的类型来处理。类型断言有两种语法:

  1. as 语法:
let someValue: any = 'this is a string';
let strLength: number = (someValue as string).length;
  1. 尖括号语法(在 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;
}

这里 ab 参数标注为 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 接口不仅拥有 nameage 属性,还新增了 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(默认)、privateprotected

  1. public:可以在类内部和外部访问。
class Person {
    public name: string;
    constructor(name: string) {
        this.name = name;
    }
}
let person = new Person('John');
console.log(person.name); 
  1. private:只能在类内部访问。
class Person {
    private name: string;
    constructor(name: string) {
        this.name = name;
    }
}
let person = new Person('John');
console.log(person.name); 
// 报错,无法在类外部访问 private 属性
  1. 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 是抽象类,它有一个抽象方法 getAreaCircle 类继承自 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 的优势。