Typescript的基本语法详解
一、TypeScript 基础类型
在 TypeScript 中,基础类型是构建更复杂类型的基石。
(一)布尔类型(boolean)
布尔类型只有两个值:true
和 false
,这与 JavaScript 中的布尔值一致。
let isDone: boolean = false;
这里我们声明了一个变量 isDone
,类型为 boolean
,并初始化为 false
。
(二)数字类型(number)
TypeScript 中的数字类型和 JavaScript 一样,都是双精度 64 位浮点值。可以表示整数和小数。
let myNumber: number = 42;
let pi: number = 3.14;
除了十进制,还支持二进制、八进制和十六进制表示。
let binaryNumber: number = 0b1010; // 二进制 1010 表示十进制 10
let octalNumber: number = 0o755; // 八进制 755 表示十进制 493
let hexadecimalNumber: number = 0xFFFF; // 十六进制 FFFF 表示十进制 65535
(三)字符串类型(string)
字符串用于表示文本数据。可以使用单引号 '
或双引号 "
来定义字符串。
let greeting: string = 'Hello, World!';
let anotherGreeting: string = "Goodbye, World!";
TypeScript 还支持模板字符串,它允许嵌入表达式。
let name: string = 'John';
let message: string = `Hello, ${name}! How are you?`;
(四)null 和 undefined
在 TypeScript 中,null
和 undefined
有自己的类型,分别为 null
和 undefined
。它们是所有类型的子类型。
let nothing: null = null;
let unassigned: undefined = undefined;
默认情况下,null
和 undefined
可以赋值给任何类型。但在严格模式下(strictNullChecks
开启),它们只能赋值给 void
和它们各自的类型。
(五)void 类型
void
类型表示没有任何类型。通常用于函数没有返回值的情况。
function logMessage(message: string): void {
console.log(message);
}
这里 logMessage
函数返回 void
,因为它没有返回值。
(六)never 类型
never
类型表示那些永不存在的值的类型。例如,一个总是抛出异常或无限循环的函数返回值类型就是 never
。
function throwError(message: string): never {
throw new Error(message);
}
function infiniteLoop(): never {
while (true) {}
}
(七)any 类型
any
类型表示任意类型。当你不确定一个值的类型时,可以使用 any
。
let value: any = 'a string';
value = 42;
value = true;
虽然 any
类型提供了很大的灵活性,但过度使用会失去 TypeScript 的类型检查优势。
(八)unknown 类型
unknown
类型和 any
类似,也表示任意类型。但与 any
不同的是,unknown
类型的值在使用前必须进行类型检查。
let something: unknown = 'a value';
// 下面这行代码会报错,因为 unknown 类型不能直接赋值给 string 类型
// let str: string = something;
if (typeof something ==='string') {
let str: string = something;
}
(九)枚举类型(enum)
枚举类型用于定义一组命名的常量。它可以让代码更易读和维护。
enum Color {
Red,
Green,
Blue
}
let myColor: Color = Color.Green;
默认情况下,枚举成员从 0
开始自动编号。也可以手动指定值。
enum Direction {
Up = 1,
Down,
Left,
Right
}
这里 Up
的值为 1
,Down
的值为 2
,Left
的值为 3
,Right
的值为 4
。
二、变量声明与类型注解
在 TypeScript 中,变量声明与 JavaScript 类似,但可以添加类型注解来明确变量的类型。
(一)变量声明方式
let
和const
let
声明的变量具有块级作用域。const
声明的是常量,一旦赋值后不能再改变。
{
let localVar: number = 10;
const constantValue: string = 'constant';
localVar = 20; // 合法
// constantValue = 'new value'; // 报错,常量不能重新赋值
}
var
var
声明的变量具有函数作用域,在现代 TypeScript 代码中,建议优先使用let
和const
。
(二)类型注解
- 基本类型注解
在声明变量时,可以在变量名后加上
:
和类型来进行类型注解。
let age: number;
age = 30;
- 复杂类型注解 对于对象、数组等复杂类型,也可以进行类型注解。
let person: { name: string; age: number };
person = { name: 'Alice', age: 25 };
- 函数参数和返回值类型注解
function add(a: number, b: number): number {
return a + b;
}
这里函数 add
的参数 a
和 b
类型为 number
,返回值类型也为 number
。
三、函数
函数在 TypeScript 中是一等公民,并且可以进行详细的类型定义。
(一)函数定义与类型
- 函数声明
function greet(name: string): string {
return `Hello, ${name}!`;
}
- 函数表达式
let greetFunc: (name: string) => string = function (name: string): string {
return `Hello, ${name}!`;
};
这里 greetFunc
是一个函数类型的变量,其类型为 (name: string) => string
,表示接受一个 string
类型参数并返回一个 string
类型值的函数。
(二)可选参数和默认参数
- 可选参数
在参数名后加上
?
表示该参数是可选的。
function printMessage(message: string, prefix?: string) {
if (prefix) {
console.log(prefix + ':'+ message);
} else {
console.log(message);
}
}
printMessage('Hello');
printMessage('World', 'Info');
- 默认参数 可以给参数设置默认值。
function greet(name = 'Guest'): string {
return `Hello, ${name}!`;
}
console.log(greet());
console.log(greet('John'));
(三)剩余参数
使用 ...
来表示剩余参数,它将所有剩余的参数收集到一个数组中。
function sum(...numbers: number[]): number {
return numbers.reduce((acc, num) => acc + num, 0);
}
console.log(sum(1, 2, 3));
(四)函数重载
函数重载允许一个函数接受不同数量或类型的参数,通过为同一个函数定义多个函数类型签名来实现。
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: any, b: any): any {
if (typeof a === 'number' && typeof b === 'number') {
return a + b;
} else if (typeof a ==='string' && typeof b ==='string') {
return a + b;
}
return null;
}
console.log(add(1, 2));
console.log(add('Hello, ', 'World'));
这里定义了两个函数重载签名,一个接受两个 number
类型参数返回 number
,另一个接受两个 string
类型参数返回 string
。实际的函数实现根据参数类型进行不同的操作。
四、数组与元组
数组和元组是 TypeScript 中常用的数据结构,它们有不同的类型表示和使用方式。
(一)数组
- 类型注解
可以使用
type[]
或Array<type>
来表示数组类型。
let numbers: number[] = [1, 2, 3];
let strings: Array<string> = ['a', 'b', 'c'];
- 数组方法的类型检查
TypeScript 会对数组的方法进行类型检查。例如,
push
方法的参数类型必须与数组元素类型一致。
let numbers: number[] = [1, 2, 3];
numbers.push(4); // 合法
// numbers.push('five'); // 报错,类型不匹配
(二)元组
元组是一种特殊的数组,它允许表示一个固定长度且元素类型已知的数组。
let point: [number, number] = [10, 20];
这里 point
是一个元组,第一个元素类型为 number
,第二个元素类型也为 number
。访问元组元素时,类型检查会确保访问的元素类型正确。
let x: number = point[0];
// let y: string = point[1]; // 报错,类型不匹配
元组还支持可选元素和剩余元素。
let user: [string, number, boolean?] = ['John', 30];
let colors: [string,...string[]] = ['red', 'green', 'blue'];
这里 user
元组的第三个元素是可选的,colors
元组第一个元素是 string
类型,后面可以有多个 string
类型的剩余元素。
五、对象类型
在 TypeScript 中,对象类型用于描述对象的结构。
(一)对象字面量类型
可以通过对象字面量来定义对象类型。
let person: { name: string; age: number } = { name: 'Bob', age: 28 };
这里定义了一个 person
对象,它必须包含 name
属性,类型为 string
,以及 age
属性,类型为 number
。
(二)接口(interface)
接口是一种更强大的方式来定义对象类型。它可以被重复使用,用于定义类的形状或对象的类型。
interface Person {
name: string;
age: number;
}
let alice: Person = { name: 'Alice', age: 25 };
接口还支持可选属性和只读属性。
interface Product {
name: string;
price: number;
description?: string;
readonly id: number;
}
let book: Product = { name: 'TypeScript Basics', price: 29.99, id: 123 };
// book.id = 456; // 报错,id 是只读属性
(三)类型别名(type alias)
类型别名也可以用于定义对象类型,与接口有一些相似之处,但也有不同。
type User = {
username: string;
email: string;
};
let newUser: User = { username: 'testuser', email: 'test@example.com' };
接口和类型别名的主要区别之一是,接口可以合并声明,而类型别名不行。
interface Point {
x: number;
}
interface Point {
y: number;
}
let myPoint: Point = { x: 10, y: 20 };
// type PointAlias {
// x: number;
// }
// type PointAlias {
// y: number;
// } // 报错,不能重复定义类型别名
六、类
类是面向对象编程的核心概念,TypeScript 对类提供了完整的支持。
(一)类的定义与实例化
- 类的定义
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound.`);
}
}
这里定义了一个 Animal
类,有一个 name
属性和一个构造函数 constructor
用于初始化 name
,还有一个 speak
方法。
2. 类的实例化
let dog = new Animal('Buddy');
dog.speak();
(二)访问修饰符
public
public
是默认的访问修饰符,表示属性或方法可以在类的内部和外部访问。
class Car {
public brand: string;
constructor(brand: string) {
this.brand = brand;
}
public displayBrand() {
console.log(`The car brand is ${this.brand}`);
}
}
let myCar = new Car('Toyota');
console.log(myCar.brand);
myCar.displayBrand();
private
private
修饰的属性或方法只能在类的内部访问。
class BankAccount {
private balance: number;
constructor(initialBalance: number) {
this.balance = initialBalance;
}
private updateBalance(amount: number) {
this.balance += amount;
}
deposit(amount: number) {
this.updateBalance(amount);
}
getBalance() {
return this.balance;
}
}
let account = new BankAccount(1000);
// console.log(account.balance); // 报错,balance 是私有属性
// account.updateBalance(500); // 报错,updateBalance 是私有方法
account.deposit(500);
console.log(account.getBalance());
protected
protected
修饰的属性或方法可以在类的内部和子类中访问。
class Shape {
protected color: string;
constructor(color: string) {
this.color = color;
}
protected displayColor() {
console.log(`The shape color is ${this.color}`);
}
}
class Circle extends Shape {
radius: number;
constructor(color: string, radius: number) {
super(color);
this.radius = radius;
}
showDetails() {
this.displayColor();
console.log(`The circle has a radius of ${this.radius}`);
}
}
let circle = new Circle('red', 5);
// console.log(circle.color); // 报错,color 是受保护属性
// circle.displayColor(); // 报错,displayColor 是受保护方法
circle.showDetails();
(三)继承
通过 extends
关键字实现类的继承。
class Vehicle {
wheels: number;
constructor(wheels: number) {
this.wheels = wheels;
}
move() {
console.log(`The vehicle with ${this.wheels} wheels is moving.`);
}
}
class Car extends Vehicle {
brand: string;
constructor(brand: string, wheels: number) {
super(wheels);
this.brand = brand;
}
drive() {
console.log(`Driving a ${this.brand} car with ${this.wheels} wheels.`);
}
}
let myCar = new Car('Ford', 4);
myCar.move();
myCar.drive();
这里 Car
类继承自 Vehicle
类,拥有 Vehicle
类的属性和方法,并添加了自己的 brand
属性和 drive
方法。
(四)抽象类
抽象类是不能被实例化的类,它通常作为其他类的基类,包含一些抽象方法。抽象方法是没有具体实现的方法,必须在子类中实现。
abstract class Figure {
abstract area(): number;
abstract perimeter(): number;
}
class Rectangle extends Figure {
width: number;
height: number;
constructor(width: number, height: number) {
super();
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
perimeter() {
return 2 * (this.width + this.height);
}
}
// let figure = new Figure(); // 报错,不能实例化抽象类
let rectangle = new Rectangle(5, 3);
console.log(rectangle.area());
console.log(rectangle.perimeter());
七、泛型
泛型是 TypeScript 中非常强大的特性,它允许我们在定义函数、类、接口时不指定具体类型,而是在使用时再指定类型。
(一)泛型函数
function identity<T>(arg: T): T {
return arg;
}
let result1 = identity<number>(42);
let result2 = identity<string>('hello');
这里定义了一个泛型函数 identity
,<T>
表示类型参数,T
可以是任何类型。在调用函数时,可以通过 <>
来指定 T
的具体类型。
(二)泛型接口
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
这里定义了一个泛型接口 GenericIdentityFn
,它描述了一个接受类型为 T
的参数并返回类型为 T
的值的函数。
(三)泛型类
class Stack<T> {
private items: T[] = [];
push(item: T) {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
let numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.pop());
let stringStack = new Stack<string>();
stringStack.push('a');
stringStack.push('b');
console.log(stringStack.pop());
这里 Stack
类是一个泛型类,<T>
表示栈中元素的类型。不同实例化的 Stack
类可以存储不同类型的元素。
(四)泛型约束
有时我们需要对泛型类型进行一些约束,比如要求传入的类型必须包含某个属性。
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
let result = loggingIdentity('hello');
// let badResult = loggingIdentity(42); // 报错,number 类型不满足 Lengthwise 接口约束
这里定义了一个接口 Lengthwise
,要求类型必须有 length
属性。泛型函数 loggingIdentity
对 T
进行了约束,只有满足 Lengthwise
接口的类型才能作为 T
的值传入。
八、类型断言
类型断言用于告诉编译器某个值的实际类型,当你比编译器更清楚某个值的类型时可以使用。
(一)语法
- 尖括号语法
let someValue: any = 'this is a string';
let strLength: number = (<string>someValue).length;
as
语法
let someValue: any = 'this is a string';
let strLength: number = (someValue as string).length;
在 TypeScript 中,当在 JSX 中使用时,只能使用 as
语法。
(二)类型断言的用途
- 将
any
类型转换为更具体的类型
function getValue(): any {
return 'a value';
}
let value = getValue();
let length = (value as string).length;
- 绕过编译器的类型检查 但要谨慎使用,因为如果断言的类型不正确,可能会导致运行时错误。
let num: number = 10;
// 下面这行代码虽然绕过了类型检查,但在运行时可能出错
let str: string = num as string;
九、类型兼容性
TypeScript 在进行类型检查时,会根据类型兼容性规则来判断一个类型是否可以赋值给另一个类型。
(一)基本类型兼容性
- 数字类型兼容性
数字类型之间是兼容的,例如
number
和number
兼容,number
和bigint
不兼容。
let num1: number = 10;
let num2: number = num1;
// let bigNum: bigint = num1; // 报错,类型不兼容
- 布尔类型兼容性
boolean
类型只有true
和false
两个值,不同的boolean
类型变量之间是兼容的。
(二)对象类型兼容性
对象类型兼容性基于结构子类型。如果一个对象类型的所有属性在另一个对象类型中都有兼容的类型,那么这两个对象类型是兼容的。
interface Point {
x: number;
}
interface Circle {
x: number;
radius: number;
}
let point: Point = { x: 10 };
let circle: Circle = { x: 10, radius: 5 };
point = circle; // 合法,Circle 类型兼容 Point 类型
// circle = point; // 报错,Point 类型不兼容 Circle 类型,缺少 radius 属性
(三)函数类型兼容性
函数类型兼容性比较复杂。对于函数参数,是双向协变的,但在严格模式下(strictFunctionTypes
开启),是逆变的。对于返回值,是协变的。
let func1: (a: number) => void = (a) => console.log(a);
let func2: (a: number | string) => void = (a) => console.log(a);
func1 = func2; // 在非严格函数类型检查下合法
// func2 = func1; // 在严格函数类型检查下报错,参数类型不兼容
这里 func2
的参数类型 number | string
比 func1
的参数类型 number
更宽泛。在非严格函数类型检查下,func2
可以赋值给 func1
,但在严格函数类型检查下不行。
十、模块
模块是 TypeScript 中组织代码的重要方式,它允许将代码分割成独立的单元。
(一)模块的定义与导出
- 导出变量、函数、类等
// utils.ts
export const PI = 3.14;
export function add(a: number, b: number) {
return a + b;
}
export class MathUtils {
static multiply(a: number, b: number) {
return a * b;
}
}
- 默认导出 一个模块只能有一个默认导出。
// greeting.ts
const greeting = 'Hello, World!';
export default greeting;
(二)模块的导入
- 导入命名导出
import { PI, add, MathUtils } from './utils';
console.log(PI);
console.log(add(2, 3));
console.log(MathUtils.multiply(4, 5));
- 导入默认导出
import greeting from './greeting';
console.log(greeting);
- 重命名导入
import { add as sum } from './utils';
console.log(sum(1, 2));
- 整体导入
import * as utils from './utils';
console.log(utils.PI);
console.log(utils.add(3, 4));
console.log(utils.MathUtils.multiply(6, 7));
通过以上对 TypeScript 基本语法的详细讲解,你应该对 TypeScript 有了更深入的理解,可以在实际项目中更好地运用它来提高代码的质量和可维护性。