TypeScript面试题
为什么推荐使用TypeScript?
TypeScript是微软公司开发和维护的一种面向对象的编程语言。它是JavaScript的超集,包含其所有元素。
其中,强类型和弱类型、静态类型和动态类型是两组不同的概念。
类型强弱是针对类型转换是否显示来区分,静态和动态类型是针对类型检查的时机来区分。
TS对JS的改进主要是静态类型检查,静态类型检查有何意义?标准答案是“静态类型更有利于构建大型应用”。
推荐使用TypeScript的原因有:
- TypeScript为JavaScript ide和实践(如静态检查)提供了高效的开发工具。
- TypeScript使代码更易于阅读和理解。
- 使用TypeScript,我们可以大大改进普通的JavaScript。
- TypeScript为我们提供了ES6(ECMAScript 6)的所有优点,以及更高的生产率。
- TypeScript通过对代码进行类型检查,可以帮助我们避免在编写JavaScript时经常遇到的令人痛苦的错误。
- 强大的类型系统,包括泛型。
- TypeScript代码可以按照ES5和ES6标准编译,以支持最新的浏览器。
- 与ECMAScript对齐以实现兼容性。
- 以JavaScript开始和结束。
- 支持静态类型。
- TypeScript将节省开发人员的时间。
说说TypeScript中命名空间与模块的理解和区别
命名空间:命名空间一个最明确的目的就是解决重名问题
命名空间定义了标识符的可见范围,一个标识符可在多个名字空间中定义,它在不同名字空间中的含义是互不相干的
这样,在一个新的名字空间中可定义任何标识符,它们不会与任何已有的标识符发生冲突,因为已有的定义都处于其他名字空间中
模块:TypeScript
与 ECMAScript
2015 一样,任何包含顶级 import
或者 export
的文件都被当成一个模块
相反地,如果一个文件不带有顶级的import
或者export
声明,那么它的内容被视为全局可见的
它们之间的区别:
- 命名空间是位于全局命名空间下的一个普通的带有名字的 JavaScript 对象,使用起来十分容易。但就像其它的全局命名空间污染一样,它很难去识别组件之间的依赖关系,尤其是在大型的应用中
- 像命名空间一样,模块可以包含代码和声明。 不同的是模块可以声明它的依赖
- 在正常的TS项目开发过程中并不建议用命名空间,但通常在通过 d.ts 文件标记 js 库类型的时候使用命名空间,主要作用是给编译器编写代码的时候参考使用
TypeScript的主要特点是什么?
- 跨平台:TypeScript 编译器可以安装在任何操作系统上,包括 Windows、macOS 和 Linux。
- ES6 特性:TypeScript 包含计划中的 ECMAScript 2015 (ES6) 的大部分特性,例如箭头函数。
- 面向对象的语言:TypeScript 提供所有标准的 OOP 功能,如类、接口和模块。
- 静态类型检查:TypeScript 使用静态类型并帮助在编译时进行类型检查。因此,你可以在编写代码时发现编译时错误,而无需运行脚本。
- 可选的静态类型:如果你习惯了 JavaScript 的动态类型,TypeScript 还允许可选的静态类型。
TypeScript支持的访问修饰符有哪些?
TypeScript支持访问修饰符 public,private 和 protected,它们决定了类成员的可访问性。
- 公共(public),类的所有成员,其子类以及该类的实例都可以访问。
- 受保护(protected),该类及其子类的所有成员都可以访问它们。 但是该类的实例无法访问。
- 私有(private),只有类的成员可以访问它们。
如果未指定访问修饰符,则它是隐式公共的,因为它符合 JavaScript 的便利性。
TypeScript中有哪些声明变量的方式?
TypeScript 中声明变量的方式与 JavaScript 基本相同,因为它是 JavaScript 的超集。主要有三种方式:var
、let
和 const
。除此之外,TypeScript 还增加了类型注解等特性。以下是每种声明方式的详细解释。
var
声明
var
是 JavaScript 最早引入的变量声明方式,在 TypeScript 中也可以使用。它有以下特点:
- 函数作用域:
var
声明的变量是函数作用域,在整个函数内部都可以访问,不受代码块的限制。 - 变量提升:
var
声明的变量会被提升到函数的顶部,即使它在使用前还没有被赋值,也不会报错,只是值为undefined
。 - 重复声明:
var
允许重复声明同一个变量,不会报错,这可能导致意外的行为。
1 | function example() { |
let
声明
let
是 ES6 引入的一种变量声明方式,在 TypeScript 中也广泛使用。它弥补了 var
的一些缺点,有以下特点:
- 块级作用域:
let
声明的变量具有块级作用域,只能在其所在的代码块{ ... }
内有效。 - 不能重复声明:在同一作用域中,
let
不能重复声明相同名称的变量。 - 变量提升但不初始化:
let
变量会被提升到作用域的顶部,但是不会自动初始化为undefined
。在声明前访问会导致ReferenceError
。
示例:
1 | function example() { |
const
声明
const
也是 ES6 引入的,用于声明常量。它有以下特点:
- 块级作用域:
const
和let
一样,也具有块级作用域,只能在其所在的代码块内有效。 - 必须初始化:
const
声明的变量必须在声明时赋值,否则会报错。 - 不能重新赋值:
const
声明的变量不能被重新赋值,但如果是对象或数组,其内部的属性或元素可以改变。 - 不可重复声明:与
let
一样,const
不能在同一作用域中重复声明。
示例:
1 | const a = 10; |
TypeScript 中的类型注解
在 TypeScript 中,除了声明变量之外,还可以为变量添加类型注解,以确保变量只能存储特定类型的值。
示例:
1 | let num: number = 10; // 声明一个数字类型的变量 |
解构赋值
TypeScript 支持解构赋值语法,可以用它来声明变量。你可以在对象和数组解构中使用 let
和 const
。
对象解构:
1 | const person = { name: "Alice", age: 25 }; |
数组解构:
1 | const numbers: number[] = [1, 2, 3]; |
类型推断
TypeScript 具有类型推断功能,可以在不显式声明类型的情况下,推断出变量的类型。
1 | let message = "Hello, world!"; // 推断为 string 类型 |
总结
- **
var
**:函数作用域,允许重复声明,有变量提升。一般在现代 TypeScript 中不推荐使用。 - **
let
**:块级作用域,不允许重复声明,有变量提升但未初始化前不能访问,推荐用于可变变量的声明。 - **
const
**:块级作用域,必须初始化,不能重新赋值,推荐用于不可变的常量声明。
在 TypeScript 中,更提倡使用 let
和 const
,并搭配类型注解来进行更严格的类型检查,提升代码的可读性和可靠性。
TypeScript中的Declare关键字有什么作用?
我们知道所有的JavaScript库/框架都没有TypeScript声明文件,但是我们希望在TypeScript文件中使用它们时不会出现编译错误。为此,我们使用declare关键字。在我们希望定义可能存在于其他地方的变量的环境声明和方法中,可以使用declare关键字。
例如,假设我们有一个名为myLibrary的库,它没有TypeScript声明文件,在全局命名空间中有一个名为myLibrary的命名空间。如果我们想在TypeScript代码中使用这个库,我们可以使用以下代码:
1 | declare let myLibrary; |
TypeScript运行时将把myLibrary变量赋值为任意类型(any)。这是一个问题,我们不会得到智能感知在设计时,但我们将能够使用库在我们的代码。
解释一下TypeScript中的枚举
枚举是TypeScipt数据类型,它允许我们定义一组命名常量。 使用枚举去创建一组不同的案例变得更加容易。 它是相关值的集合,可以是数字值或字符串值。
1 | enum Gender { |
TypeScript中的装饰器
TypeScript 中的装饰器(Decorators)是一个实验性的特性,它允许你通过一种声明方式来修改类、方法、属性或参数的行为。装饰器本质上是一个函数,可以用于在类或类的成员上添加额外的功能,比如日志记录、权限控制、数据验证等。
装饰器的类型
在 TypeScript 中,装饰器主要分为以下几种:
- 类装饰器(Class Decorators)
- 方法装饰器(Method Decorators)
- 访问器装饰器(Accessor Decorators)
- 属性装饰器(Property Decorators)
- 参数装饰器(Parameter Decorators)
启用装饰器
要在 TypeScript 中使用装饰器,需要在 tsconfig.json
中开启 experimentalDecorators
选项:
1 | { |
类装饰器
类装饰器应用于类的定义上,可以用于修改类的构造函数或添加额外的功能。它的装饰器函数接收一个参数:目标类的构造函数。
示例:
1 | // 类装饰器函数 |
在这个例子中,@Logger
装饰器在类 Person
被定义时调用,并输出类的名称。
类装饰器的返回值
类装饰器可以返回一个新的构造函数来替换原始类,实现类的扩展或修改行为。
1 | function WithAge(constructor: Function) { |
方法装饰器
方法装饰器用于修饰类的实例方法,它可以用来修改或扩展方法的行为。它接收三个参数:
target
:类的原型对象。propertyKey
:方法的名称。descriptor
:方法的属性描述符,可以用于修改方法的行为。
示例:
1 | function LogMethod( |
在这个例子中,@LogMethod
装饰器在方法调用前后添加了日志记录。
访问器装饰器
访问器装饰器可以用于类的 getter 或 setter,它接收三个参数:
target
:类的原型对象。propertyKey
:属性名称。descriptor
:属性的描述符。
示例:
1 | function LogAccessor( |
在这个例子中,@LogAccessor
装饰器会在 area
访问器被访问时记录日志。
属性装饰器
属性装饰器用于修饰类的属性,接收两个参数:
target
:类的原型对象。propertyKey
:属性的名称。
属性装饰器没有属性描述符,因此无法直接修改属性的行为。它通常用于添加元数据或用于依赖注入。
示例:
1 | function LogProperty(target: any, propertyKey: string) { |
参数装饰器
参数装饰器用于修饰方法的参数,接收三个参数:
target
:类的原型对象。propertyKey
:方法的名称。parameterIndex
:参数在方法中的索引。
示例:
1 | function LogParameter( |
装饰器的执行顺序
当多个装饰器同时应用到一个类上时,装饰器的执行顺序是从下到上,而对于方法和属性装饰器,执行顺序是从外到内。
1 | function ClassDecorator1() { /*...*/ } |
执行顺序为:
MethodDecorator2
MethodDecorator1
ClassDecorator2
ClassDecorator1
总结
- 类装饰器:修饰类,可以修改类的行为。
- 方法装饰器:修饰方法,常用于日志记录或修改方法的行为。
- 访问器装饰器:修饰 getter 或 setter,通常用于数据监控。
- 属性装饰器:修饰属性,常用于元数据注入。
- 参数装饰器:修饰方法的参数,常用于依赖注入或参数验证。
TypeScript中的模块是什么?
TypeScript 中的模块用于组织和封装代码。模块可以包含变量、函数、类、接口等,它们默认在模块作用域内,不会泄露到全局作用域,除非你明确地导出它们。这样做的目的是为了避免全局命名空间污染,同时也使得代码更加可维护、易于重用。
模块的导出与导入
导出(Export)
在 TypeScript 模块中,你可以使用 export
关键字来导出模块的成员(如变量、函数、类、接口等),使得它们可以在其他模块中被导入和使用。
1 | // example.ts |
导入(Import)
当你想在一个模块中使用另一个模块导出的成员时,你可以使用 import
关键字来导入这些成员。
1 | // anotherModule.ts |
TypeScript的内置数据类型有哪些?
TypeScript 提供了一系列内置的基础数据类型,用于类型检查和确保代码的健壮性。以下是 TypeScript 中的主要内置数据类型:
number
- 用于表示数字类型,包括整数和浮点数。
1 | let age: number = 25; |
string
表示字符串类型。
1
let name: string = "Alice";
boolean
- 表示布尔值,
true
或false
。
1 | let isActive: boolean = true; |
null
表示一个空值。
1
let value: null = null;
undefined
表示一个未定义的值。
1
let value: undefined = undefined;
void
- 通常用于表示函数没有返回值的情况。
1 | function logMessage(message: string): void { |
any
表示任意类型,允许禁用类型检查。这种类型可以接受任何值,但一般不推荐使用,因为会丧失 TypeScript 的类型安全优势。
1
2let something: any = 5;
something = "now a string";
unknown
- 表示未知类型,比
any
更安全。与any
不同,在使用unknown
类型的值之前,必须进行类型检查。
1 | let value: unknown = "Hello"; |
never
表示永远不会有值的类型,通常用于函数抛出异常或永远不会返回的情况。
1
2
3function throwError(message: string): never {
throw new Error(message);
}
object
用于表示非原始类型的对象(即函数、数组、对象等)。
1
let person: object = { name: "Alice", age: 25 };
array
- 表示数组类型。可以使用两种方式定义数组的类型:
number[]
:表示一个数字数组。Array<number>
:泛型语法。
1 | let numbers: number[] = [1, 2, 3]; |
元组(tuple
)
元组是一种固定长度的数组,其每个元素的类型是已知的,可以各不相同。
1
2let tuple: [string, number];
tuple = ["Alice", 25]; // 正确
enum
枚举类型用于定义一组命名常量。
1
2
3
4
5
6enum Color {
Red,
Green,
Blue
}
let color: Color = Color.Green;
bigint
表示任意精度的整数,可以处理比
number
类型更大的整数。1
let bigNumber: bigint = 100n;
字面量类型(Literal Types)
字面量类型允许使用具体的字符串、数字或布尔值作为类型。
1
2let direction: "up" | "down";
direction = "up"; // 只能是 "up" 或 "down"
联合类型(Union Types)
联合类型允许变量可以是多种类型之一。
1
2
3let value: string | number;
value = "Hello";
value = 123;
交叉类型(Intersection Types)
交叉类型用于将多个类型合并为一个类型,表示必须同时满足多个类型的要求。
1
2
3type Person = { name: string };
type Employee = Person & { employeeId: number };
let worker: Employee = { name: "Alice", employeeId: 123 };
总结
TypeScript 的内置类型涵盖了 JavaScript 中的基础数据类型,并扩展了额外的类型(如 enum
、tuple
、unknown
和 never
等),这些类型让 TypeScript 可以更好地进行静态类型检查和确保代码的安全性与健壮性。
TypeScript中never和void的区别?
在 TypeScript 中,never
和 void
都表示某种程度的“无返回值”或“无值”,但它们的含义和用途是不同的。以下是它们之间的主要区别:
void
void
表示函数 没有返回值,或表示一个变量可能没有用处的场景。
用途
- 函数没有返回值的情况
当一个函数没有返回任何值时(即返回undefined
),可以使用void
作为返回类型。 - 通常表示某个操作没有返回有意义的值,但实际在运行时,
void
类型的函数实际上会返回undefined
。
1 | function logMessage(message: string): void { |
void
也可以用于变量声明中,表示该变量不应该包含任何有意义的值(尽管它可以是undefined
或null
)。
运行时表现
void
类型的函数在运行时 不会真正返回值,但 **实际上隐式返回undefined
**。
总结
void
用于表示 函数没有返回有意义的值。- 在函数执行完毕后,隐式返回
undefined
。
never
never
表示的是 永远不会有返回值。它用于表示 那些永远无法正常完成执行 的函数或代码路径,例如抛出异常或者无限循环的函数。
用途
- 用于永远不会正常返回的函数。典型场景包括:
- 抛出错误的函数。
- 无限循环的函数。
1 | function throwError(message: string): never { |
运行时表现
never
类型的函数在运行时 不会有返回,函数要么 抛出错误,要么 进入无限循环。- 它表示 代码不可能到达 的场景,函数不会正常执行结束。
总结
never
表示一个函数永远不会有任何形式的返回值,也不会返回undefined
。通常用于表示异常终止的代码路径。
never
与 void
的区别
特性 | void |
never |
---|---|---|
含义 | 没有返回有意义的值,返回 undefined 。 |
永远不会返回值(不会结束、抛异常或进入死循环)。 |
用途 | 用于函数无返回值的情况。 | 用于无法正常结束的函数(如抛异常、无限循环)。 |
运行时表现 | 函数可以执行结束并隐式返回 undefined 。 |
函数永远不会返回,即执行中途出错或一直运行不结束。 |
示例 | void 用于无返回值的函数。 |
never 用于抛异常或无限循环的函数。 |
类型的意图 | 表示可以正常执行但没有返回值。 | 表示函数无法正常结束,也不返回值。 |
结论:
void
表示函数 可以执行完毕,但没有返回值(即隐式返回undefined
)。never
则表示 函数永远不会执行结束(如抛出异常或进入死循环),因此 不会有任何返回值。
TypeScript中的类型断言是什么?
类型断言(Type Assertion) 是 TypeScript 中的一种机制,允许开发者通过显式的方式告诉编译器某个值的类型。在某些情况下,开发者比 TypeScript 编译器更清楚某个变量的确切类型,因此可以使用类型断言来覆盖 TypeScript 的默认推断结果。
类型断言不会进行运行时的类型检查或转换,它只在编译时起作用,用来告诉编译器“请把这个变量当作某个特定的类型处理”。这在处理来自外部 API 的数据或者绕过类型推断不准确的场景中非常有用。
使用场景
- 当 TypeScript 无法自动推断出正确的类型。
- 当需要明确地告诉编译器某个值的类型。
TypeScript 提供了两种语法来进行类型断言:
尖括号语法:
1
2let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;as
语法(推荐):1
2let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
as
语法与尖括号语法的区别:
as
语法 更加通用,尤其在 React 的 JSX 语法中,尖括号语法会与 JSX 语法冲突,因此推荐使用as
语法。
将 any
类型的值断言为 string
类型:
1 | let value: any = "hello world"; |
处理 DOM 元素:
在 TypeScript 中,document.getElementById
的返回值类型是 HTMLElement | null
,如果确定该元素存在并且是特定的元素类型(例如 HTMLInputElement
),可以使用类型断言:
1 | let inputElement = document.getElementById("user-input") as HTMLInputElement; |
将联合类型断言为更具体的类型:
如果一个变量的类型是联合类型(如 string | number
),可以使用类型断言将其断言为更具体的类型:
1 | function getLength(input: string | number): number { |
注意事项
- 类型断言仅在开发者确信类型的情况下使用。如果断言错误,编译器不会报错,但在运行时可能会导致意外的行为。
- 类型断言并不会进行实际的类型转换,只是在编译时帮助确定类型。因此,类型断言并不会改变值的原始类型。
总结
类型断言在 TypeScript 中是一个有用的工具,尤其在处理复杂数据类型或从外部接口获取数据时。它可以帮助开发者明确告知编译器某个值的确切类型,从而避免不必要的编译错误。但类型断言的使用应谨慎,以避免潜在的类型错误。
TS中any和unknown有什么区别?
unknown 和 any 的主要区别是 unknown 类型会更加严格:在对 unknown 类型的值执行大多数操作之前,我们必须进行某种形式的检查。而在对 any 类型的值执行操作之前,我们不必进行任何检查。
1 | let foo: any = 123; |
因为bar是一个未知类型(任何类型的数据都可以赋给 unknown
类型),所以不能确定是否有msg属性。不能通过TS语法检测;而 unknown 类型的值也不能将值赋给 any 和 unknown 之外的类型变量
总结: any 和 unknown 都是顶级类型,但是 unknown 更加严格,不像 any 那样不做类型检查,反而 unknown 因为未知性质,不允许访问属性,不允许赋值给其他有明确类型的变量。
使用TS实现一个判断传入参数是否是数组类型的方法?
unknown 用于变量类型不确定,但肯定可以确定的情形下,比如下面这个示例中,参数总归会有个值,根据这个值的类型进行不同的处理,这里使用 unknown 替代 any 则会更加类型安全。
1 | function isArray(x: unknown): boolean { |
tsconfig.json有什么作用?
tsconfig.json文件是JSON格式的文件。
在tsconfig.json文件中,可以指定不同的选项来告诉编译器如何编译当前项目。
目录中包含tsconfig.json文件,表明该目录是TypeScript项目的根目录。
1 | // 常用配置 |
TypeScript中什么是类类型接口?
- 如果接口用于一个类的话,那么接口会表示“行为的抽象”
- 对类的约束,让类去实现接口,类可以实现多个接口
- 接口只能约束类的公有成员(实例属性/方法),无法约束私有成员、构造函数、静态属性/方法
TS中什么是方法重载?
在 TypeScript 中,方法重载(Function Overloading) 是指为同一个函数提供 多个不同的函数签名,以支持不同数量或类型的参数。通过方法重载,TypeScript 可以根据传入参数的不同来调用对应的函数实现。这使得同一个函数可以处理多种不同的输入形式,增强了代码的灵活性和可读性。
方法重载的主要作用:
- 允许同一个函数可以有多个不同的参数类型组合。
- 提高代码的可读性,特别是当函数需要处理多种不同的输入时。
- 可以为函数提供不同的调用方式,提供更灵活的接口。
TypeScript 中的方法重载实现
在 TypeScript 中,方法重载是通过 声明多个函数签名 来实现的,而实际的函数实现只会有一个。编译器会根据传入的参数匹配相应的签名,从而调用对应的实现。
语法结构
- 函数重载签名:这些是函数的不同签名声明,用于描述不同的参数和返回类型。
- 函数实现:只有一个函数实现,这个实现包含了处理所有重载的逻辑。
1 | // 定义多个函数重载签名 |
解释
- 这里有三个重载签名:
add(x: number, y: number): number;
—— 当传入两个number
类型参数时,返回一个number
。add(x: string, y: string): string;
—— 当传入两个string
类型参数时,返回一个string
。add(x: number, y: string): string;
—— 当传入一个number
和一个string
时,返回一个string
。
- 函数的实际实现接收任意类型的参数(
x: any, y: any
),并根据参数的类型来处理不同的情况。
使用示例
1 | console.log(add(5, 10)); // 输出: 15 |
在这个例子中,add
函数根据不同类型的输入执行不同的逻辑:
- 如果传入的是两个数字,它们相加并返回。
- 如果传入的是两个字符串,它们进行字符串拼接。
- 如果是一个数字和一个字符串,数字被转换为字符串后与另一个字符串拼接。
注意事项
- 函数实现必须兼容所有重载签名:函数的实际实现应该能够处理所有重载签名中列出的参数类型。
- 顺序问题:重载签名的顺序可以影响编译器选择合适的重载。通常最具体的重载签名应该放在前面。
- 返回值类型的兼容性:不同的重载签名可以有不同的返回类型,但函数实现需要能正确处理这些返回值。
方法重载的实际意义:
- 处理多种输入类型:通过方法重载,你可以让同一个函数处理不同的输入类型,而无需定义多个不同名称的函数。
- 改善代码的可读性:可以使用相同的函数名称,而不必为每种参数类型创建不同的函数名称。
- 类型安全性:TypeScript 编译器会根据重载签名进行类型检查,确保函数调用时传入的参数和返回的值符合预期。
总结
在 TypeScript 中,方法重载 通过定义多个函数签名来允许一个函数根据不同的输入类型或参数数量来执行不同的操作。实际的函数实现只写一次,TypeScript 编译器会根据传入的参数选择合适的重载来进行类型检查。
TS中的类是什么,如何定义?
类表示一组相关对象的共享行为和属性。
例如,我们的类可能是Student,其所有对象都具有该attendClass方法。另一方面,John是一个单独的 type 实例,Student可能有额外的独特行为,比如attendExtracurricular.
你使用关键字声明类class:
1 | class Student { |
如何在TS中实现继承?
继承是一种从另一个类获取一个类的属性和行为的机制。它是面向对象编程的一个重要方面,并且具有从现有类创建新类的能力,继承成员的类称为基类,继承这些成员的类称为派生类。
继承可以通过使用extend关键字来实现。我们可以通过下面的例子来理解它。
1 | class Shape { |
TS中的泛型是什么?如何理解?
在 TypeScript 中,泛型(Generics) 是一种让你在编写代码时能够创建更加灵活、可重用的组件的机制。泛型允许你编写的函数、类或接口可以适应多个不同的类型,而不是限定为某一个具体的类型。通过泛型,你可以让代码在处理不同类型的数据时仍然保持强类型检查。
为什么需要泛型?
有时候你编写的函数、类或接口可能需要处理多种类型的数据,而不是固定某一种类型。为了让代码更加灵活和可重用,泛型提供了一种在不牺牲类型安全性的前提下,使代码能够处理多种类型的方式。
没有泛型 vs. 使用泛型
没有泛型的函数:
假设我们需要编写一个函数,它接受一个数组,并返回数组中的第一个元素。
1 | function getFirstElement(arr: any[]): any { |
在这个例子中,我们使用 any
类型来定义参数和返回值,这样函数可以接受任何类型的数组。然而,any
类型会 丢失类型信息,使得编译器无法检查返回值的类型,可能导致运行时的错误:
1 | const firstNumber = getFirstElement([1, 2, 3]); |
使用泛型的函数
现在我们使用泛型来重写这个函数,确保它能够处理不同类型的数组,同时保持类型安全:
1 | function getFirstElement<T>(arr: T[]): T { |
在这个例子中:
T
是一个 泛型参数,代表函数可以处理的任意类型。arr: T[]
表示这个函数接收一个类型为T
的数组。T
作为返回值的类型,确保我们返回的值与输入的数组元素类型相同。
这样,TypeScript 在编译时就能够知道 getFirstElement
返回的具体类型,因此可以避免可能的运行时错误。
1 | firstNumber.toFixed(); // 类型为 number,没问题 |
如何理解泛型?
泛型可以理解为在编写代码时对类型进行参数化。你可以通过泛型来定义一个“占位符”类型,然后在使用时再指定具体的类型。就像函数可以接收参数并返回不同的结果,泛型让类型也能够“参数化”。
泛型的使用场景
泛型函数
上面提到的例子就是一个泛型函数。再看一个更复杂的例子:
1 | function identity<T>(arg: T): T { |
这个函数 identity
接受一个类型为 T
的参数,并返回类型为 T
的结果。使用时可以通过 <T>
指定具体的类型。
泛型接口
你可以在接口中使用泛型来定义更加灵活的接口:
1 | interface Box<T> { |
这里,Box
接口接收一个类型参数 T
,表示这个 Box
中的 contents
属性可以是任意类型。
泛型类
泛型同样可以应用于类:
1 | class GenericNumber<T> { |
在这个例子中,GenericNumber
类使用泛型 T
,表示它可以处理任意类型的 value
,并且 add
方法可以处理 T
类型的两个参数。
泛型约束
有时,你可能希望对泛型进行约束,要求传入的类型必须满足某些条件。可以使用 extends
关键字来实现。
例如,如果我们希望一个函数只接受拥有 length
属性的参数:
1 | interface HasLength { |
在这个例子中,T
被约束为必须具有 length
属性,因此 logLength
函数可以安全地访问 arg.length
。
泛型默认值
你可以为泛型提供默认类型,如果调用泛型时没有指定类型参数,那么会使用默认值:
1 | function createArray<T = string>(length: number, value: T): T[] { |
总结
- 泛型 是 TypeScript 中用于编写更具复用性、类型安全的代码的机制。
- 泛型让你在编写函数、接口、类时不必预先指定类型,而是在使用时指定具体类型。
- 泛型可以帮助你创建更灵活和强大的组件,同时保持代码的类型安全性和可维护性。
通过泛型,你可以避免在代码中使用 any
这样的类型,而保留类型检查和推断的好处,从而编写更加健壮和灵活的 TypeScript 代码。
说说TS中的类及其特性
TypeScript 引入了类,以便它们可以利用诸如封装和抽象之类的面向对象技术的好处。
TypeScript 编译器将 TypeScript 中的类编译为普通的 JavaScript 函数,以跨平台和浏览器工作。
一个类包括以下内容:
- 构造器(Constructor)
- 属性(Properties)
- 方法(Methods)
1 | class Employee { |
类的其他特性有:
- 继承(Inheritance)
- 封装(Encapsulation)
- 多态(Polymorphism)
- 抽象(Abstraction)
解释如何使用TypeScript mixin
Mixin 本质上是在相反方向上工作的继承。Mixins 允许你通过组合以前类中更简单的部分类来设置构建新类。
相反,类A继承类B来获得它的功能,类B从类A需要返回一个新类的附加功能。
什么是TypeScript映射文件?
- TypeScript Map文件是一个源映射文件,其中包含有关我们原始文件的信息。
- .map文件是源映射文件,可让工具在发出的JavaScript代码和创建它的TypeScript源文件之间进行映射。
- 许多调试器可以使用这些文件,因此我们可以调试TypeScript文件而不是JavaScript文件。
TS中的interface和type有什么区别?
相同点:
- 都可以描述一个对象或者函数
interface
1 | interface User { |
type
1 | type User = { |
- 都允许拓展(extends)
interface 和 type 都可以拓展,并且两者并不是相互独立的,也就是说 interface 可以 extends type, type 也可以 extends interface 。 虽然效果差不多,但是两者语法不同。
interface:可以被扩展和实现(implements)。使用 extends 关键词可以扩展一个或多个接口,这对于定义类或对象的类型非常有用。接口也是更适合在声明 API 类型时使用的,因为它们可以通过声明合并来扩展。
1 | interface Animal { |
type:虽然通过交叉类型(&)可以达到类似扩展的效果,但 type 本身不支持声明合并,因此在某些情况下它的扩展性不如 interface。
1 | type Animal = { |
interface:允许重复声明同一个接口,并且它们会自动合并。这使得在多个文件中或者在类型声明的逐步扩展中非常有用。
1 | interface Window { |
type:不允许重复声明。尝试为同一个 type 声明两次会导致错误。
1 | type Window = { |
不同点
- type 可以而 interface 不行
type 可以声明基本类型别名,联合类型,元组等类型
1 | // 基本类型别名 |
type 语句中还可以使用 typeof 获取实例的类型进行赋值
1 | // 当你想获取一个变量的类型时,使用 typeof |
其他骚操作
1 | type StringOrNumber = string | number; |
- interface 可以而 type 不行
interface 能够声明合并
1 | interface User { |
总结
虽然 interface 和 type 在很多情况下可以互换使用,了解它们的差异可以帮助你更好地决定在特定情况下使用哪一个。一般来说,如果你需要声明对象的结构或扩展现有的类型,并且这些类型可能会在不同的地方被扩展或重复声明,那么 interface 是一个更好的选择。如果你需要使用联合或交叉类型,或者你的类型是基本类型的别名,那么 type 可能是更合适的选项。
TS中的getter/setter是什么?你如何使用它们
Getter 和 setter 是特殊类型的方法,可帮助你根据程序的需要委派对私有变量的不同级别的访问。
Getters 允许你引用一个值但不能编辑它。Setter 允许你更改变量的值,但不能查看其当前值。这些对于实现封装是必不可少的。
例如,新雇主可能能够了解get公司的员工人数,但无权set了解员工人数。
1 | const fullNameMaxLength = 10; |
如何检查TS中的null和undefiend?
通过使用一个缓冲检查,我们可以检查空和未定义:
1 | if (x == null) { } |
如果我们使用严格的检查,它将总是对设置为null的值为真,而对未定义的变量不为真。
1 | var a: number; |
输出
1 | "a == null" |
TypeScript中const和readonly的区别是什么?
在 TypeScript 中,const
和 readonly
都用于定义不可更改的值,但它们的应用场景和作用范围有所不同。下面是两者的区别和详细解释。
const
的作用
- 用于变量声明:
const
关键字用于声明常量变量,这意味着一旦给它赋值后,不能再重新赋值。 - 块级作用域:
const
具有块级作用域(类似于let
),即它只在声明的代码块中有效。 - 初始化后不可重新赋值:
const
声明的变量必须在声明时初始化,并且不能再次重新赋值(但如果它是对象,属性可以改变,参见下面的解释)。
1 | const x = 10; |
- 在这个例子中,
x
被声明为常量,不能重新赋值。 - 但是
obj
是一个对象,虽然不能更改obj
本身的引用(即不能把obj
重新指向另一个对象),但它的属性是可以修改的。
readonly
的作用
- 用于类和接口的属性:
readonly
关键字主要用于类和接口的属性,确保这些属性一旦被初始化后,就不能再被修改。 - 只能设置一次:
readonly
属性可以在声明时初始化,也可以在构造函数中赋值,但之后不能再更改。 - 适用于对象属性:
readonly
是用来防止修改对象属性或类成员的,但它不会影响对象本身的可变性。即使某个属性是readonly
,如果这个属性是一个对象,那么该对象的内容仍然是可以更改的。
1 | class Person { |
在这个例子中,Person
类的 name
属性是 readonly
,因此一旦在构造函数中被赋值后,就不能再修改。
1 | interface Point { |
- 在接口中,
readonly
确保对象属性不能被修改。
const
和 readonly
的主要区别:
特性 | const |
readonly |
---|---|---|
作用域 | 变量作用域 | 类、接口的属性 |
可用位置 | 函数或代码块中声明变量 | 类、接口中的属性声明 |
赋值时间 | 必须在声明时赋值,不能再重新赋值 | 可以在声明时或构造函数中赋值,但不能在其他地方修改 |
应用对象 | 整个变量不能重新赋值,但对象属性可以修改 | 属性一旦赋值不可修改,但对象的内部状态可变 |
常见场景 | 常量、不可更改的变量 | 不可修改的对象或类的属性 |
总结
const
用于变量声明,表示变量的引用不可以改变,但它并不会使对象的内容变为不可变。readonly
用于类、接口中的属性,确保属性一旦初始化后不可更改。它的应用范围比const
更窄,专门用于对象或类的属性。
这两者的核心区别在于,const
是针对变量的,而 readonly
是针对对象或类的属性,用于控制属性的不可变性。
Omit 类型有什么作用
在 TypeScript 中,
Omit
类型是一个实用工具类型,用于创建一个新的类型,该类型基于现有类型,并从中 移除一些指定的属性。这是特别有用的,当你想要在保留大部分原始类型结构的情况下排除某些属性时。
Omit
类型的定义
1 | type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>; |
T
是原始类型。K
是你想要从T
中移除的属性集合。Exclude<keyof T, K>
表示从T
的所有键中排除掉K
中的键。Pick<T, ...>
是用来选择T
中剩余的键,形成新的类型。
使用场景
Omit
类型适用于以下场景:
- 从一个接口或类型中移除某些属性。
- 在某些情况下不需要或者不允许某些属性,可以创建一个新类型,使得那些属性不可用。
Omit
类型的用法
假设你有一个接口定义一个用户对象,你想要创建一个不包含敏感信息的用户类型:
1 | interface User { |
在这个例子中,PublicUser
类型是基于 User
类型创建的,但不包含 password
属性。
多个属性的 Omit
你可以一次性移除多个属性,只需用逗号分隔它们即可:
1 | type LimitedUser = Omit<User, 'password' | 'email'>; |
Omit
与 Partial
、Pick
等工具类型的区别
- **
Partial<T>
**:将类型T
中的所有属性变为可选。 - **
Pick<T, K>
**:从类型T
中选择某些属性来构建一个新的类型。 - **
Omit<T, K>
**:从类型T
中移除某些属性来构建一个新的类型。
1 | type OptionalUser = Partial<User>; // User 的所有属性变为可选 |
Omit
可以看作是 Pick
的补集,Pick
选择属性,而 Omit
则排除属性。
组合使用 Omit
有时候你可能会结合使用 Omit
与其他工具类型。假设你有一个特定的场景,需要将某些属性移除并且剩下的属性变为可选,可以这样做:
1 | type OptionalPublicUser = Partial<Omit<User, 'password'>>; |
总结
- **
Omit<T, K>
**:Omit
是一个工具类型,用于从类型T
中移除属性K
,生成一个新的类型。 - 它可以帮助你简化类型定义,尤其是在需要从现有类型中排除某些不需要的属性时。
Omit
可以有效地管理代码,减少重复定义类型,提高代码的可读性和维护性。
Omit
在实际开发中非常实用,比如处理 API 数据时去除敏感信息,或者在继承某些类型的过程中有选择性地去掉某些属性。