TypeScript基础
类型推断
TypeScript的类型推断是TypeScript编译器自动分析代码并确定变量或表达式的类型的能力。这种能力减少了显式类型注解的需要,使得代码更加简洁,同时也保持了TypeScript的类型安全性。
基本类型推断
当你声明一个变量并给它赋值时,TypeScript编译器会查看这个值的类型,并尝试推断出变量的类型。例如:
let age = 30; // 推断为number类型
let name = "Alice"; // 推断为string类型
let isDone = true; // 推断为boolean类型
在这些例子中,变量age
、name
和isDone
的类型分别是number
、string
和boolean
,这些类型都是基于它们的初始值自动推断出来的。
上下文类型推断
在某些情况下,TypeScript会根据上下文来推断类型。这通常发生在函数调用、赋值表达式或函数返回值的上下文中。
函数调用中的参数类型推断
function greet(name: string) {
console.log(`Hello, ${name}!`);
}
// 参数类型推断
greet("Alice"); // 正确,因为"Alice"是string类型
// greet(42); // 错误,因为42不是string类型
在这个例子中,greet
函数的参数name
被明确指定为string
类型。当我们调用greet
函数并传入一个值时,TypeScript会检查这个值是否符合string
类型的要求。
赋值表达式中的类型推断
let value;
value = "Hello, TypeScript!"; // 推断为string类型
value = 42; // 现在value的类型被更新为number,因为最新的赋值是number类型
在这个例子中,变量value
最初没有明确的类型。但是,当我们给它赋值时,TypeScript会根据赋值的类型来推断出value
的类型。注意,由于TypeScript的类型检查是在编译时进行的,所以value
的类型会根据它最后一次赋值的类型来确定。
数组和元组的类型推断
对于数组和元组,TypeScript也会根据它们包含的元素来推断类型。
let numbers = [1, 2, 3, 4, 5]; // 推断为number[]
let people = [{ name: "Alice" }, { name: "Bob" }]; // 推断为{ name: string; }[]
let pair: [string, number]; // 明确指定为元组类型
pair = ["Alice", 30]; // 正确
// pair = [30, "Alice"]; // 错误,因为元组元素的顺序和类型需要匹配
泛型中的类型推断
泛型函数或泛型类的类型参数也可以被自动推断。
function identity<T>(arg: T): T {
return arg;
}
let output = identity("Hello, TypeScript!"); // 推断T为string
let outputNum = identity(42); // 推断T为number
在这个例子中,identity
函数是一个泛型函数,它接受一个类型为T
的参数并返回相同类型的值。当我们调用identity
函数时,TypeScript会根据传入的参数自动推断出T
的类型。
总的来说,TypeScript的类型推断功能非常强大,它能够在很大程度上减少显式类型注解的需要,同时保持类型安全。然而,在某些复杂的情况下,你可能仍然需要显式地指定类型注解来确保代码的正确性。
类型注解
类型注解(Type Annotations)是TypeScript(TS)语言中的一个重要特性,它允许开发者显式地指定变量、函数参数、函数返回值等处的类型信息。通过类型注解,TypeScript编译器能够在编译时进行类型检查,提供更好的代码提示和错误检测,从而帮助开发者编写更加健壮和可维护的代码。
类型注解的基本用法
类型注解通常使用冒号:
后跟一个类型来指定。例如:
let age: number = 30; // 指定变量age的类型为number
function greet(name: string): void { // 指定函数greet的参数name的类型为string,返回值的类型为void
console.log("Hello, " + name + "!");
}
类型注解的适用场景
- 变量声明:指定变量的类型,以便在后续使用中保持类型一致。
- 函数参数:指定函数参数的类型,确保传递给函数的参数符合预期的类型。
- 函数返回值:指定函数返回值的类型,让调用者知道函数将返回什么类型的值。
- 对象属性:在接口或类型别名中指定对象属性的类型,确保对象结构的正确性。
TypeScript中的常见类型
- 基本类型:包括
number
、string
、boolean
、null
、undefined
、symbol
、bigint
等。 - 对象类型:包括数组、对象字面量、函数等。在TypeScript中,对象类型通常通过接口(
interface
)或类型别名(type
)来定义。 - 特殊类型:如
any
(任意类型)、void
(无返回值类型)、never
(永不存在的值的类型)等。 - 复合类型:如联合类型(
|
)、交叉类型(&
)、元组([]
)等,用于表示更复杂的类型组合。
类型注解的优势
- 提高代码的可读性和可维护性:通过显式地指定类型,开发者可以更容易地理解代码的意图和结构。
- 增强类型安全性:类型注解可以在编译时捕获类型错误,减少运行时错误的发生。
- 支持智能提示和自动完成:在支持TypeScript的编辑器中,类型注解可以提供智能的代码提示和自动完成功能,提高开发效率。
注意事项
- 并非所有情况下都需要使用类型注解。在TypeScript中,如果变量有初始值,编译器会尝试进行类型推断。然而,在某些复杂或不明确的情况下,显式地指定类型注解可以提高代码的清晰度和准确性。
- 类型注解应该与代码的实际逻辑保持一致。如果类型注解与代码的实际行为不符,可能会导致编译错误或运行时错误。
总之,类型注解是TypeScript中一个非常有用的特性,它可以帮助开发者编写更加健壮、可维护和易于理解的代码。
类型断言
类型断言(Type Assertion)是TypeScript中的一个特性,它允许开发者手动指定一个值的类型,以覆盖TypeScript编译器的默认类型推断。这可以帮助解决编译器因为缺乏足够信息而无法正确推断类型时的问题,或者当开发者确信某个值的类型与编译器推断的类型不符时,可以通过类型断言来明确告诉编译器该值的实际类型。
类型断言的用途
- 将一个联合类型断言为其中一个类型:
当TypeScript遇到一个联合类型的变量时,它默认只能访问这个联合类型所有类型共有的属性和方法。如果需要访问某个类型特有的属性或方法,可以使用类型断言将该变量断言为那个特定的类型。 - 将一个父类断言为更加具体的子类:
在类继承关系中,如果父类没有子类特有的属性或方法,但开发者确信某个父类类型的变量实际上是某个子类的实例,可以通过类型断言来访问子类的特有属性和方法。 - 解决类型推断不准确的问题:
在某些情况下,TypeScript的类型推断可能不够准确,导致无法访问某些属性或方法。此时,可以使用类型断言来明确指定变量的类型,从而解决编译错误。
类型断言的语法
在TypeScript中,类型断言有两种语法形式:
- 尖括号语法(
<类型>值
):
这种语法在早期版本中广泛使用,但由于与JSX语法冲突(在JSX中尖括号用于表示HTML元素),因此在使用React或JSX的项目中可能会受到限制。 as
语法(值 as 类型
):
这是推荐的语法形式,因为它与JSX兼容,且更加清晰易读。
示例
// 使用as语法进行类型断言
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function isFish(animal: Cat | Fish): boolean {
return typeof (animal as Fish).swim === 'function';
}
const pet: Cat = { name: 'Tom', run: () => { console.log('run'); } };
// 这里调用isFish时,即使pet是Cat类型,也不会在编译时报错
// 但运行时如果调用pet.swim会抛出TypeError
console.log(isFish(pet)); // 输出: false
注意事项
- 类型断言不会改变值的实际类型:它只是告诉TypeScript编译器在编译时如何理解这个值。
- 滥用类型断言可能导致运行时错误:如果断言的类型与实际类型不符,运行时可能会出现问题。
- 类型断言是一种权宜之计:在可能的情况下,应该尽量通过改进代码结构或类型定义来避免使用类型断言。
总之,类型断言是TypeScript中一个强大的特性,但也需要谨慎使用,以避免引入不必要的错误和复杂性。
基本类型
数据类型 | 关键字 | 描述 |
---|---|---|
任意类型 | any | 声明为 any 的变量可以赋予任意类型的值。 |
数字类型 | number | 双精度 64 位浮点值。它可以用来表示整数和分数。let binaryLiteral: number = 0b1010; // 二进制 let octalLiteral: number = 0o744; // 八进制 let decLiteral: number = 6; // 十进制 let hexLiteral: number = 0xf00d; // 十六进制 |
字符串类型 | string | 一个字符系列,使用单引号(')或双引号(")来表示字符串类型。反引号(****)来定义多行文本和内嵌表达式。 let name: string = “Runoob”; let years: number = 5; let words: string = 您好,今年是 ${ name } 发布 ${ years + 1} 周年 ;` |
布尔类型 | boolean | 表示逻辑值:true 和 false。let flag: boolean = true; |
数组类型 | 无 | 声明变量为数组。// 在元素类型后面加上[] let arr: number[] = [1, 2]; // 或者使用数组泛型 let arr: Array<number> = [1, 2]; |
元组 | 无 | 元组类型用来表示已知元素数量和类型的数组,各元素的类型不必相同,对应位置的类型需要相同。let x: [string, number]; x = ['Runoob', 1]; // 运行正常 x = [1, 'Runoob']; // 报错 console.log(x[0]); // 输出 Runoob |
枚举 | enum | 枚举类型用于定义数值集合。enum Color {Red, Green, Blue}; let c: Color = Color.Blue; console.log(c); // 输出 2 |
void | void | 用于标识方法返回值的类型,表示该方法没有返回值。function hello(): void { alert("Hello Runoob"); } |
null | null | 表示对象值缺失。 |
undefined | undefined | 用于初始化变量为一个未定义的值 |
never | never | never 是其它类型(包括 null 和 undefined)的子类型,代表从不会出现的值。 |
**注意:**TypeScript 和 JavaScript 没有整数类型。
枚举
在TypeScript中,枚举(Enum)是一种特殊的数据类型,用于定义一组命名的常量值。枚举可以帮助开发者更清晰地表达意图,提高代码的可读性和可维护性。以下是对TypeScript中枚举的详细解析:
枚举的基本语法
枚举使用enum
关键字来定义。例如:
enum Direction {
Up,
Down,
Left,
Right
}
这段代码定义了一个名为Direction
的枚举类型,其中包含四个常量:Up
、Down
、Left
和Right
。默认情况下,这些常量的值从0
开始自动递增。
枚举的特性
- 自动递增:如果枚举成员没有显式地初始化,则它们的值会自动从
0
开始递增。 - 手动设置初始值:可以手动为枚举成员设置初始值,后续成员的值将基于该初始值递增。
- 字符串枚举:枚举成员的值也可以是字符串。在字符串枚举中,每个成员的值都必须显式指定,且成员之间不会自动递增。
- 混合枚举(不推荐):虽然TypeScript允许创建混合数字和字符串的枚举,但这样做通常会使枚举的用途变得模糊,且可能导致意外的行为。
- 反向映射:TypeScript枚举支持反向映射,即可以通过枚举值获取其对应的常量名称。但请注意,字符串枚举不支持反向映射。
- 常量枚举:使用
const
关键字定义的枚举称为常量枚举。常量枚举在编译时会被删除,并内联到引用它们的地方,这有助于提高性能和减少生成的代码大小。 - 外部枚举:使用
declare enum
定义的枚举称为外部枚举。外部枚举用于描述已经存在的枚举类型的形状,它们通常用于类型声明文件中,以提供对外部JavaScript代码的类型支持。
枚举的使用
枚举成员可以直接通过枚举类型来访问,例如:Direction.Up
。也可以通过枚举的值来访问对应的枚举成员名称(如果支持反向映射的话)。
示例
数字枚举示例
enum NumberEnum {
First = 0,
Second = 1,
Third, // 自动赋值为 2
Fourth // 自动赋值为 3
}
console.log(NumberEnum.Third); // 输出: 2
字符串枚举示例
enum StringEnum {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT"
}
console.log(StringEnum.Up); // 输出: "UP"
常量枚举示例
const enum Directions {
Up,
Down,
Left,
Right
}
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
// 编译后的代码类似于:let directions = [0, 1, 2, 3];
注意事项
- 枚举成员的值必须是常量表达式或另一个枚举成员。
- 字符串枚举不支持反向映射。
- 尽量避免在枚举中使用混合类型的成员(即既有数字又有字符串),因为这可能会使枚举的用途变得模糊。
- 外部枚举主要用于类型声明文件中,以提供对外部JavaScript代码的类型支持。在普通TypeScript代码中,通常使用非外部枚举。
函数
函数(Function)是编程中的一个基本概念,它是一段组织好的、可重复使用的、用来实现单一或相关联功能的代码块。函数能够提高代码的重用性、模块化和可读性。在大多数编程语言中,包括 TypeScript(作为 JavaScript 的超集),函数是基本的构建块之一。
函数的基本组成
函数通常由以下几个部分组成:
- 函数声明/定义:这是函数的实际代码,指定了函数做什么。
- 函数名:给函数指定一个唯一的名称,以便在程序中引用它。
- 参数列表:函数可以接受一个或多个参数,这些参数是传递给函数的数据,用于执行函数体内的代码。参数是可选的。
- 函数体:包含一系列语句,定义了函数的具体功能。
- 返回值:函数可以返回一个值给调用者。返回值是可选的,如果函数不返回任何值,则通常返回
undefined
(在 JavaScript 和 TypeScript 中)。
函数的类型
在 TypeScript 中,函数的类型非常灵活,你可以通过多种方式定义函数类型:
- 函数声明:使用
function
关键字声明函数。 - 函数表达式:将函数赋值给变量或作为参数传递给其他函数。
- 箭头函数:ES6 引入的一种更简洁的函数写法,用于非方法函数,并且不绑定自己的
this
、arguments
、super
或new.target
。 - 函数类型别名:使用
type
关键字为复杂的函数类型创建一个别名。
示例
下面是一个简单的 TypeScript 函数示例:
// 函数声明
function greet(name: string): string {
return `Hello, ${name}!`;
}
// 调用函数
console.log(greet("Alice")); // 输出: Hello, Alice!
// 函数表达式
const greetFunc = function(name: string): string {
return `Hello, ${name}!`;
};
console.log(greetFunc("Bob")); // 输出: Hello, Bob!
// 箭头函数
const greetArrow = (name: string): string => `Hello, ${name}!`;
console.log(greetArrow("Charlie")); // 输出: Hello, Charlie!
// 函数类型别名
type Greeter = (name: string) => string;
const greetWithTypeAlias: Greeter = (name) => `Hello, ${name}!`;
console.log(greetWithTypeAlias("David")); // 输出: Hello, David!
函数的特性
- 封装:函数封装了实现细节,只暴露必要的接口给外部。
- 重用:一旦定义了函数,就可以在程序的多个地方调用它,而无需重复编写相同的代码。
- 模块化:将复杂的程序分解为一系列函数,有助于理解和维护。
- 抽象:函数隐藏了实现细节,只关注于做什么而不是怎么做。
函数是编程中不可或缺的一部分,掌握函数的用法对于编写高效、可维护的代码至关重要。
接口
在 TypeScript(以及许多其他面向对象的编程语言中),接口(Interfaces)是一种强大的特性,它允许你定义一个对象的形状,即该对象应该有哪些属性和方法。接口通常被用作类型注解,以确保类的实例或对象字面量符合特定的结构。
接口可以定义:
- 属性(可以是可选的)
- 方法(也可以是可选的,但如果你不打算在接口中直接实现它们,则它们通常会被视为类的“契约”,即类必须实现这些方法)
基本用法
interface Person {
name: string;
age: number;
greet: () => void; // 或者使用方法语法:greet(): void;
}
// 实现接口的类
class Employee implements Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
}
// 使用接口作为类型注解
const employee: Person = new Employee("Alice", 30);
employee.greet(); // 输出: Hello, my name is Alice and I am 30 years old.
在这个例子中,Person
接口定义了三个成员:name
(一个字符串类型的属性)、age
(一个数字类型的属性)和 greet
(一个不接受参数且不返回任何值的方法)。Employee
类通过 implements
关键字表明它实现了 Person
接口,这意味着它必须提供接口中定义的所有属性和方法。
可选属性和只读属性
接口中的属性可以是可选的,也可以是只读的。
interface Person {
readonly id: number; // 只读属性
name: string;
age?: number; // 可选属性
}
let person: Person = {
id: 1,
name: "Bob"
// 注意:我们没有提供 age,因为它是可选的
};
// person.id = 2; // 这将导致编译错误,因为 id 是只读的
索引签名
接口也可以定义索引签名,这允许你使用类似数组或对象的索引来访问对象的属性。
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
类类型
接口还可以描述类的实例应该具有哪些属性和方法,但这通常是通过类实现接口来完成的。然而,你也可以定义一个接口,它描述了一个类的“形状”,包括构造函数签名和实例成员。
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
tick(): void;
}
class Clock implements ClockInterface {
constructor(public hour: number, public minute: number) {}
tick() {
console.log("Beep beep!");
}
}
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
return new ctor(hour, minute);
}
const clock = createClock(Clock, 12, 17);
clock.tick(); // 输出: Beep beep!
在这个例子中,ClockConstructor
接口定义了一个构造函数签名,该签名与 Clock
类的构造函数相匹配。这使得 createClock
函数能够接受任何符合该构造函数签名的类作为参数,并返回该类的实例,该实例实现了 ClockInterface
接口。
类型别名
在 TypeScript 中,类型别名(Type Aliases)是一种为复杂类型提供一个简单名称的方式。类型别名通过 type
关键字来定义,这使得代码更加简洁、易于理解和维护。
类型别名可以引用任何类型,包括原始类型、联合类型、交叉类型、对象类型字面量、泛型类型等。
基本用法
type Name = string;
type NameResolver = () => string; //表示一个函数,该函数不接受任何参数,并且返回一个字符串(由 string 表示)。
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): string {
if (typeof n === 'string') {
return n;
}
return n();
}
// 使用
const name: Name = "Alice";
const resolver: NameResolver = () => "Bob";
console.log(getName(name)); // 输出: Alice
console.log(getName(resolver)); // 输出: Bob
在这个例子中,Name
是 string
类型的别名,NameResolver
是返回 string
的函数类型的别名,而 NameOrResolver
是 Name
和 NameResolver
的联合类型的别名。
对象类型别名
类型别名也可以用于创建对象类型的别名,这对于定义复杂的数据结构特别有用。
type User = {
id: number;
name: string;
isAdmin: boolean;
};
const user: User = {
id: 1,
name: "Alice",
isAdmin: false,
};
泛型类型别名
类型别名也可以包含泛型类型参数,这使得它们可以更加通用和灵活。
type Container<T> = {
value: T;
};
const numContainer: Container<number> = {
value: 123,
};
const strContainer: Container<string> = {
value: "hello",
};
在这个例子中,Container
是一个泛型类型别名,它接受一个类型参数 T
,并定义了一个包含 value
属性的对象类型,其中 value
的类型是 T
。
交叉类型与类型别名
类型别名也可以与交叉类型结合使用,以创建更加复杂的类型。
type LoggedFunction = (() => void) & {
logName: string;
};
const myFunction: LoggedFunction = function() {
console.log('Hello!');
} as any;
myFunction.logName = 'myFunction';
// 注意:上面的例子使用了 `as any` 来绕过 TypeScript 的类型检查,因为通常 TypeScript 编译器无法直接推断出函数类型与额外属性的组合。
// 在实际应用中,你可能需要定义一个函数工厂或使用类来更自然地表达这种结构。
尽管交叉类型和类型别名结合使用时可能会遇到一些限制,但它们仍然是 TypeScript 中非常强大的特性,可以帮助你构建复杂而灵活的类型系统。
泛型
TypeScript 中的泛型(Generics)是一种强大的工具,它允许你定义函数、接口和类时,不具体指定将要操作的数据的类型。相反,你在使用它们时指定类型。这样,你就可以创建可重用的组件,这些组件可以工作于多种数据类型上。
泛型函数
泛型函数允许你在函数签名中定义一个或多个类型参数。这些类型参数在函数体内被用作类型注解。
function identity<T>(arg: T): T {
return arg;
}
let output = identity<string>("myString"); // 类型参数为 string
let outputNum = identity(42); // TypeScript 会自动推断出 T 为 number
泛型接口
泛型接口允许你定义一个接口,这个接口可以工作于多种类型上。
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
泛型类
泛型类允许你在类级别上定义类型参数。
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
constructor(zero: T, add: (x: T, y: T) => T) {
this.zeroValue = zero;
this.add = add;
}
zero(): T {
return this.zeroValue;
}
add(x: T, y: T): T {
return this.add(x, y);
}
}
let myGenericNumber = new GenericNumber<number>(0, function(x, y) { return x + y; });
console.log(myGenericNumber.zero()); // 输出: 0
console.log(myGenericNumber.add(2, 3)); // 输出: 5
泛型约束
有时,你可能想对泛型进行约束,以确保它符合特定的结构。你可以通过 extends
关键字来实现这一点。
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}