typeScript
- 1.什么是TypeScript?
- 是什么?
- 特性?
- 区别?
- 2.TypeScript数据类型?
- 3.说说你对 TypeScript 中枚举类型的理解?应用场景?
- 4.说说你对 TypeScript 中接口的理解?应用场景?
- 使用方法
- 5.说说你对 TypeScript 中类的理解?应用场景?
- 使用方法
- 6.修饰符
- 私有修饰符
- 受保护修饰符
- 静态属性
- 7.抽象类
- 8.说说你对 TypeScript 中函数的理解?与 JavaScript 函数的区别?
- 使用方式
- 可选参数
- 剩余类型
- 函数重载
- 区别
- 9.说说你对 TypeScript 中泛型的理解?应用场景?
- 使用方式
- 函数声明
- 接口声明
- 10.说说你对 TypeScript 中高级类型的理解?有哪些?
- 交叉类型
- 联合类型
- 类型别名
- 类型索引
- 类型约束
- 映射类型
- 条件类型
- 11.说说你对 TypeScript 装饰器的理解?应用场景?
- 使用方式
- 类装饰
- 方法/属性装饰
- 参数装饰
- 访问器装饰
- 装饰器工厂
- 执行顺序
- 12.说说对 TypeScript 中命名空间与模块的理解?区别?
- 13.typescript 中的 is 关键字有什么用?
- 14.ts中any和unknown有什么区别?
- 总结
- 15.如何将 unknown 类型指定为一个更具体的类型?
- 16.类型断言
- 17.void和never区别
- 18.函数如何声明数据类型
- 19.tscongfig.json作用
- 20.如何检测null和undefined
- 21.非空断言
- 22.ts对象混入
- 23.type和interface区别
- 24.ts变量及变量提升
- 25.ts装饰器
- 26.ts混入
- 27.封装map方法
1.什么是TypeScript?
是什么?
TypeScript
是 JavaScript
的类型的超集,支持ES6
语法,支持面向对象编程的概念,如类、接口、继承、泛型等 。其是一种静态类型检查的语言,提供了类型注解,在代码编译阶段就可以检查出数据类型的错误
同时扩展了JavaScript
的语法,所以任何现有的JavaScript
程序可以不加改变的在 TypeScript
下工作,为了保证兼容性,TypeScript
在编译阶段需要编译器编译成纯 JavaScript
来运行
特性?
- 类型批注和编译时类型检查 :在编译时批注变量类型
- 类型推断:ts 中没有批注变量类型会自动推断变量的类型
- 类型擦除:在编译过程中批注的内容和接口会在运行时利用工具擦除
- 接口:ts 中用接口来定义对象类型
- 枚举:用于取值被限定在一定范围内的场景
- Mixin:可以接受任意类型的值
- 泛型编程:写代码时使用一些以后才指定的类型
- 名字空间:名字只在该区域内有效,其他区域可重复使用该名字而不冲突
- 元组:元组合并了不同类型的对象,相当于一个可以装不同类型数据的数组
区别?
- TypeScript 是 JavaScript 的超集,扩展了 JavaScript 的语法
- TypeScript 可处理已有的 JavaScript 代码,并只对其中的 TypeScript 代码进行编译
- TypeScript 文件的后缀名 .ts (.ts,.tsx,.dts),JavaScript 文件是 .js
- 在编写 TypeScript 的文件的时候就会自动编译成 js 文件
2.TypeScript数据类型?
-
当然,请看下面的例子:
- boolean(布尔类型): 表示逻辑值,可以是 true 或 false。用于表示条件的真假或开关状态。
let isTrue: boolean = true; let isFalse: boolean = false;
- number(数字类型): 表示数值,包括整数和浮点数。可用于表示计数、金额、坐标等数值数据。
let count: number = 10; let pi: number = 3.14;
- string(字符串类型): 表示文本数据,由字符组成的序列。用于存储和操作文本信息,如名称、描述、消息等。
let message: string = "Hello, TypeScript!"; let name: string = "John Doe";
- array(数组类型): 表示一个元素的集合,并且同一数组中的元素具有相同的类型。常用于存储列表、集合或一组相关数据
let numbers: number[] = [1, 2, 3, 4, 5]; let names: string[] = ["Alice", "Bob", "Charlie"];
- tuple(元组类型): 表示固定长度和类型的数组,各个元素的类型不必相同。用于存储具有固定位置和含义的数据集合。
let person: [string, number] = ["John Doe", 30];
- enum(枚举类型): 用于定义一组具名的常量值,方便使用这些常量的名称而不是硬编码的值。用于表示具有离散取值的情况,比如颜色选项、状态类型等。
enum Color { Red, Green, Blue, } let myColor: Color = Color.Red;
- any(任意类型): 表示所有可能的类型都可以赋值给它,它关闭了编译时类型检查,可灵活处理不确定类型的情况。适用于需要动态类型或与现有代码兼容性要求较高的情况
let value: any = 123; value = "hello"; value = true;
- null 和 undefined 类型: 表示变量可以为 null 或 undefined,通常用于表示一个值缺失或未赋值的状态。
let nullValue: null = null; let undefinedValue: undefined = undefined;
- void 类型: 用于标识方法返回值的类型,表示该方法没有返回值。常用于定义没有返回值的函数或方法。
function greet(): void { console.log("Hello!"); }
- never 类型: 表示那些永远不会出现的类型,通常用于函数永不返回和抛出异常的场景。
function throwError(message: string): never { throw new Error(message); }
- object 对象类型: 表示非原始类型的对象,如数组、函数、类等。可用于存储和操作复杂结构的数据。
let person: object = { name: "John Doe", age: 30, };
这些是对每种类型的简单举例,可以根据需要进行使用和扩展。
3.说说你对 TypeScript 中枚举类型的理解?应用场景?
在TypeScript中,枚举类型用于表示一组具名的常量值。它们允许我们定义一组有序、离散的值,并为这些值分配有意义的名称。
枚举类型的语法如下:
enum Color {
Red,
Green,
Blue,
}
在这个例子中,Color 是一个枚举类型,它包含了三个值:Red、Green、Blue。默认情况下,枚举成员从0开始自动递增,所以 Red 的值为0,Green 的值为1,Blue 的值为2。
枚举类型在以下情况下非常有用:
- 表示一组相关的常量: 枚举可以用来表示一组相关的常量,例如表示颜色、星期几、方向等。通过使用枚举,提供了更明确和可读性更高的代码。
- 避免使用魔法数值: 使用枚举可以避免直接在代码中使用魔法数值(未经解释的硬编码数字),使代码更具可维护性。
- 枚举成员的值可以自定义: 枚举成员的值不仅可以自动递增,还可以手动赋值。这样可以灵活地指定特定的值,满足特定的需求。
- 支持反向映射: 枚举类型是双向映射的,即可以根据枚举成员的名称获取对应的值,也可以根据值获取对应的枚举成员名称。这在某些情况下非常有用,例如根据颜色代码获取颜色名称。
示例代码:
enum Color {
Red = '#FF0000',
Green = '#00FF00',
Blue = '#0000FF',
}
console.log(Color.Red); // 输出:#FF0000
console.log(Color[0]); // 输出:Red
需要注意的是,枚举类型的值只在运行时有意义,编译后的JavaScript代码会将枚举转换为普通的对象,不会像TypeScript中那样使用特定的枚举类型。
总而言之,枚举类型在TypeScript中用于定义一组具名的常量值,可提高代码的可读性和可维护性,并支持自定义值和反向映射。它们在表示离散取值的场景下非常有用。
4.说说你对 TypeScript 中接口的理解?应用场景?
个接口所描述的是一个对象相关的属性和方法,但并不提供具体创建此对象实例的方法typescript
的核心功能之一就是对类型做检测,
使用方法
interface User {
name: string
age?: number//可选参数
readonly isMale: boolean//只读参数
say: (words: string) => string//函数
}
const fn = (user: User) => user.name
fn(name:'lisi')
接口还可以继承
interface Father {
color: String
}
interface Mother {
height: Number
}
interface Son extends Father,Mother{
name: string
age: Number
}
5.说说你对 TypeScript 中类的理解?应用场景?
在 ES6
之后,JavaScript
拥有了 class
关键字,虽然本质依然是构造函数,但是使用起来已经方便了许多,但是JavaScript
的class
依然有一些特性还没有加入,比如修饰符和抽象类,TypeScript
的 class
支持面向对象的所有特性,比如 类、接口等
使用方法
定义类的关键字为 class
,后面紧跟类名,类可以包含以下几个模块(类的数据成员):
- 字段 : 字段是类里面声明的变量。字段表示对象的有关数据。
- 构造函数: 类实例化时调用,可以为类的对象分配内存。
- 方法: 方法为对象要执行的操作
class Car {
// 字段
engine:string;
// 构造函数
constructor(engine:string) {
this.engine = engine
}
// 方法
disp():void {
console.log("发动机为 : "+this.engine)
}
}
继承
class Animal {
move(distanceInMeters: number = 0) {
console.log(`Animal moved${distanceInMeters}m.`);
}
}
class Dog extends Animal {
bark() {
console.log('Woof! Woof!');
}
}
const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();
Dog
是一个 派生类,它派生自 Animal
基类,派生类通常被称作子类,基类通常被称作 超类
Dog
类继承了Animal
类,因此实例dog
也能够使用Animal
类move
方法
同样,类继承后,子类可以对父类的方法重新定义,这个过程称之为方法的重写,通过super
关键字是对父类的直接引用,该关键字可以引用父类的属性和方法,
class PrinterClass {
doPrint():void {
console.log("父类的 doPrint() 方法。")
}
}
class StringPrinter extends PrinterClass {
doPrint():void {
super.doPrint() // 调用父类的函数
console.log("子类的 doPrint()方法。")
}
}
6.修饰符
- 公共 public:可以自由的访问类程序里定义的成员
- 私有 private:只能够在该类的内部进行访问
- 受保护 protect:除了在该类的内部可以访问,还可以在子类中仍然可以访问
- 只读修饰符readonly:表示属性只能在创建对象时或构造函数中进行赋值,之后不能修改。
- 静态属性static:表示属性或方法属于类本身而不是类的实例。可以通过类名直接访问静态成员,无需实例化对象。
私有修饰符
只能够在该类的内部进行访问,实例对象并不能够访问
class Father {
private name: String
constructor(name: String) {
this.name = name
}
}
const father = new Father('huihui')
//属性“name”为私有属性,只能在类“Father"中访问。 ts(2341)(property) Father.name: String
father.name
并且继承该类的子类并不能访问
class Father {
private name: String
constructor(name: String) {
this.name = name
}
}
class Son extends Father{
say() {
console.log('my name is ${this.name}')//获取不到name
}
受保护修饰符
跟私有修饰符很相似,实例对象同样不能访问受保护的属性
class Father{
protected name: String
constructor(name: String) {
this.name = name
}
}
const father = new Father('huihui')
father.name//name获取不到
在子类中可以获取到
class Father {
protected name: String
constructor(name: String) {
this.name = name
}
}
class Son extends Fatherfsay() {
say(){
console.log('my name is $(this.name')//可以获取到
}
}
静态属性
这些属性存在于类本身上面而不是类的实例上,通过static
进行定义,访问这些属性需要通过 类型.静态属性 的这种形式访问
class Square {
static width = '100px'
}
console.log(Square.width) // 100px
7.抽象类
抽象类做为其它派生类的基类使用,它们一般不会直接被实例化,不同于接口,抽象类可以包含成员的实现细节,abstract
关键字是用于定义抽象类和在抽象类内部定义抽象方法,
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log('roaming the earch...');
}
}
这种类并不能被实例化,通常需要我们创建子类去继承
class Cat extends Animal {
makeSound() {
console.log('miao miao')
}
}
const cat = new Cat()
cat.makeSound() // miao miao
cat.move() // roaming the earch...
8.说说你对 TypeScript 中函数的理解?与 JavaScript 函数的区别?
函数是JavaScript
应用程序的基础,帮助我们实现抽象层、模拟类、信息隐藏和模块,在TypeScript
里,虽然已经支持类、命名空间和模块,但函数仍然是主要定义行为的方式,TypeScript
为 JavaScript
函数添加了额外的功能,丰富了更多的应用场景
使用方式
跟javascript
定义函数十分相似,可以通过funciton
关键字、箭头函数等形式去定
const add = (a: number, b: number) => a + b
当我们没有提供函数实现的情况下,有两种声明函数类型的方式
// 方式一
type LongHand = {
(a: number): number;
};
// 方式二
type ShortHand = (a: number) => number;
可选参数
当函数的参数可能是不存在的,只需要在参数后面加上 ?
代表参数可能不存在,
const add = (a: number, b?: number) => a + (b ? b : 0)
剩余类型
剩余参数与JavaScript
的语法类似,需要用 ...
来表示剩余参数
如果剩余参数 rest
是一个由number
类型组成的数组
const add = (a: number, ...rest: number[]) => rest.reduce(((a, b) => a + b), a)
函数重载
允许创建数项名称相同但输入输出类型或个数不同的子程序,它可以简单地称为一个单独功能可以执行多项任务的能力
关于typescript
函数重载,必须要把精确的定义放在前面,最后函数实现时,需要使用 |
操作符或者?
操作符,把所有可能的输入类型全部包含进去
function add (arg1: string, arg2: string): string
function add (arg1: number, arg2: number): number
// 因为我们在下边有具体函数的实现,所以这里并不需要添加 declare 关键字
// 下边是实现
function add (arg1: string | number, arg2: string | number) {
// 在实现上我们要注意严格判断两个参数的类型是否相等,而不能简单的写一个 arg1 + arg2
if (typeof arg1 === 'string' && typeof arg2 === 'string') {
return arg1 + arg2
} else if (typeof arg1 === 'number' && typeof arg2 === 'number') {
return arg1 + arg2
}
}
区别
- 从定义的方式而言,typescript 声明函数需要定义参数类型或者声明返回值类型
- typescript 在参数中,添加可选参数供使用者选择
- typescript 增添函数重载功能,使用者只需要通过查看函数声明的方式,即可知道函数传递的参数个数以及类型
9.说说你对 TypeScript 中泛型的理解?应用场景?
泛型程序设计(generic programming)是程序设计语言的一种风格或范式,泛型允许我们在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型 在typescript
中,定义函数,接口或者类的时候,不预先定义好具体的类型,而在使用的时候在指定类型的一种特性
假设我们用一个函数,它可接受一个 number
参数并返回一个number
参数,如下写法:
function returnItem (para: number): number {
return para
}
如果我们打算接受一个 string
类型,然后再返回 string
类型,则如下写法:
function returnItem (para: string): string {
return para
}
上述两种编写方式,存在一个最明显的问题在于,代码重复度比较高
虽然可以使用 any
类型去替代,但这也并不是很好的方案,因为我们的目的是接收什么类型的参数返回什么类型的参数,即在运行时传入参数我们才能确定类型,这种情况就可以使用泛型,
function returnItem<T>(para: T): T {
return para
}
使用方式
泛型通过<>
的形式进行表述,可以声明:
- 函数
- 接口
- 类
函数声明
声明函数的形式如下:
function returnItem<T>(para: T): T {
return para
}
定义泛型的时候,可以一次定义多个类型参数,比如我们可以同时定义泛型 T
和 泛型 U
:
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]];
}
swap([7, 'seven']); // ['seven', 7]
接口声明
声明接口的形式如下:
interface ReturnItemFn<T> {
(para: T): T
}
那么当我们想传入一个number作为参数的时候,就可以这样声明函数:
const returnItem: ReturnItemFn<number> = para => para
10.说说你对 TypeScript 中高级类型的理解?有哪些?
除了string
、number
、boolean
这种基础类型外,在 typescript
类型声明中还存在一些高级的类型应用
这些高级类型,是typescript
为了保证语言的灵活性,所使用的一些语言特性。这些特性有助于我们应对复杂多变的开发场景
常见的高级类型有如下:
- 交叉类型
- 联合类型
- 类型别名
- 类型索引
- 类型约束
- 映射类型
- 条件类型
交叉类型
通过 &
将多个类型合并为一个类型,包含了所需的所有类型的特性,本质上是一种并的操作
T & U
联合类型
联合类型的语法规则和逻辑 “或” 的符号一致,表示其类型为连接的多个类型中的任意一个,本质上是一个交的关系
T | U
类型别名
类型别名会给一个类型起个新名字,类型别名有时和接口很像,但是可以作用于原始值、联合类型、元组以及其它任何你需要手写的类型
可以使用 type SomeName = someValidTypeAnnotation
的语法来创建类型别名:
type some = boolean | string
const b: some = true // ok
const c: some = 'hello' // ok
const d: some = 123 // 不能将类型“123”分配给类型“some”
此外类型别名可以是泛型:
type Container<T> = { value: T };
也可以使用类型别名来在属性里引用自己:
type Tree<T> = {
value: T;
left: Tree<T>;
right: Tree<T>;
}
可以看到,类型别名和接口使用十分相似,都可以描述一个对象或者函数
两者最大的区别在于,interface
只能用于定义对象类型,而 type
的声明方式除了对象之外还可以定义交叉、联合、原始类型等,类型声明的方式适用范围显然更加广泛
类型索引
keyof
类似于 Object.keys
,用于获取一个接口中 Key 的联合类型。
interface Button {
type: string
text: string
}
type ButtonKeys = keyof Button
// 等效于
type ButtonKeys = "type" | "text"
类型约束
通过关键字 extend
进行约束,不同于在 class
后使用 extends
的继承作用,泛型内使用的主要作用是对泛型加以约束
type BaseType = string | number | boolean
// 这里表示 copy 的参数
// 只能是字符串、数字、布尔这几种基础类型
function copy<T extends BaseType>(arg: T): T {
return arg
}
类型约束通常和类型索引一起使用,例如我们有一个方法专门用来获取对象的值,但是这个对象并不确定,我们就可以使用 extends
和 keyof
进行约束。
function getValue<T, K extends keyof T>(obj: T, key: K) {
return obj[key]
}
const obj = { a: 1 }
const a = getValue(obj, 'a')
映射类型
通过 in
关键字做类型的映射,遍历已有接口的 key
或者是遍历联合类型,如下例子:
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
interface Obj {
a: string
b: string
}
type ReadOnlyObj = Readonly<Obj>
上述的结构,可以分成这些步骤:
- keyof T:通过类型索引 keyof 的得到联合类型 ‘a’ | ‘b’
- P in keyof T 等同于 p in ‘a’ | ‘b’,相当于执行了一次 forEach 的逻辑,遍历 ‘a’ | ‘b’
所以最终ReadOnlyObj
的接口为下述:
interface ReadOnlyObj {
readonly a: string;
readonly b: string;
}
条件类型
条件类型的语法规则和三元表达式一致,经常用于一些类型不确定的情况。
T extends U ? X : Y
上面的意思就是,如果 T 是 U 的子集,就是类型 X,否则为类型 Y
11.说说你对 TypeScript 装饰器的理解?应用场景?
装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上
是一种在不改变原类和使用继承的情况下,动态地扩展对象功能
同样的,本质也不是什么高大上的结构,就是一个普通的函数,@expression
的形式其实是Object.defineProperty
的语法糖
expression
求值后必须也是一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入
使用方式
由于typescript
是一个实验性特性,若要使用,需要在tsconfig.json
文件启动,如下:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
typescript
装饰器的使用和javascript
基本一致
类的装饰器可以装饰:
- 类
- 方法/属性
- 参数
- 访问器
类装饰
例如声明一个函数 addAge
去给 Class 的属性 age
添加年龄.
function addAge(constructor: Function) {
constructor.prototype.age = 18;
}
@addAge
class Person{
name: string;
age!: number;
constructor() {
this.name = 'huihui';
}
}
let person = new Person();
console.log(person.age); // 18
上述代码,实际等同于以下形式:
Person = addAge(function Person() { ... });
上述可以看到,当装饰器作为修饰类的时候,会把构造器传递进去。 constructor.prototype.age
就是在每一个实例化对象上面添加一个 age
属性
方法/属性装饰
同样,装饰器可以用于修饰类的方法,这时候装饰器函数接收的参数变成了:
- target:对象的原型
- propertyKey:方法的名称
- descriptor:方法的属性描述符
可以看到,这三个属性实际就是Object.defineProperty
的三个参数,如果是类的属性,则没有传递第三个参数
如下例子:
// 声明装饰器修饰方法/属性
function method(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log(target);
console.log("prop " + propertyKey);
console.log("desc " + JSON.stringify(descriptor) + "\n\n");
descriptor.writable = false;
};
function property(target: any, propertyKey: string) {
console.log("target", target)
console.log("propertyKey", propertyKey)
}
class Person{
@property
name: string;
constructor() {
this.name = 'huihui';
}
@method
say(){
return 'instance method';
}
@method
static run(){
return 'static method';
}
}
const xmz = new Person();
// 修改实例方法say
xmz.say = function() {
return 'edit'
输出如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-88tgDVu0-1692176481624)(C:\Users\l\AppData\Roaming\Typora\typora-user-images\1691724716880.png)]
参数装饰
接收3个参数,分别是:
- target :当前对象的原型
- propertyKey :参数的名称
- index:参数数组中的位置
function logParameter(target: Object, propertyName: string, index: number) {
console.log(target);
console.log(propertyName);
console.log(index);
}
class Employee {
greet(@logParameter message: string): string {
return `hello ${message}`;
}
}
const emp = new Employee();
emp.greet('hello');
输入如下图:
访问器装饰
使用起来方式与方法装饰一致,如下:
function modification(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
console.log(target);
console.log("prop " + propertyKey);
console.log("desc " + JSON.stringify(descriptor) + "\n\n");
};
class Person{
_name: string;
constructor() {
this._name = 'huihui';
}
@modification
get name() {
return this._name
}
}
装饰器工厂
如果想要传递参数,使装饰器变成类似工厂函数,只需要在装饰器函数内部再函数一个函数即可,如下:
function addAge(age: number) {
return function(constructor: Function) {
constructor.prototype.age = age
}
}
@addAge(10)
class Person{
name: string;
age!: number;
constructor() {
this.name = 'huihui';
}
}
let person = new Person();
执行顺序
当多个装饰器应用于一个声明上,将由上至下依次对装饰器表达式求值,求值的结果会被当作函数,由下至上依次调用,例如如下:
function f() {
console.log("f(): evaluated");
return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("f(): called");
}
}
function g() {
console.log("g(): evaluated");
return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("g(): called");
}
}
class C {
@f()
@g()
method() {}
}
// 输出
f(): evaluated
g(): evaluated
g(): called
f(): called
12.说说对 TypeScript 中命名空间与模块的理解?区别?
ypeScript
与ECMAScript
2015 一样,任何包含顶级 import
或者 export
的文件都被当成一个模块
相反地,如果一个文件不带有顶级的import
或者export
声明,那么它的内容被视为全局可见的
例如我们在在一个 TypeScript
工程下建立一个文件 1.ts
,声明一个变量a
const a = 1
然后在另一个文件同样声明一个变量a
,这时候会出现错误信息
提示重复声明a
变量,但是所处的空间是全局的
如果需要解决这个问题,则通过import
或者export
引入模块系统即可,如下:
const a = 10;
export default a
在typescript
中,export
关键字可以导出变量或者类型,用法与es6
模块一致,如下:
export const a = 1
export type Person = {
name: String
}
通过import
引入模块,如下:
import { a, Person } from './export';
13.typescript 中的 is 关键字有什么用?
TypeScript 中的 is
关键字用于类型保护,可以在运行时判断一个对象是否属于某个类型,并根据不同的类型执行不同的逻辑。
具体来说,is
关键字通常和 instanceof
运算符一起使用,用于判断一个对象是否是某个类的实例。
14.ts中any和unknown有什么区别?
unknown 和 any 的主要区别是 unknown 类型会更加严格:在对 unknown 类型的值执行大多数操作之前,我们必须进行某种形式的检查。而在对 any 类型的值执行操作之前,我们不必进行任何检查。
let foo: any = 123;
console.log(foo.msg); // 符合TS的语法
let a_value1: unknown = foo; // OK
let a_value2: any = foo; // OK
let a_value3: string = foo; // OK
let bar: unknown = 222; // OK
console.log(bar.msg); // Error
let k_value1: unknown = bar; // OK
let K_value2: any = bar; // OK
let K_value3: string = bar; // Error
因为bar是一个未知类型(任何类型的数据都可以赋给 unknown
类型),所以不能确定是否有msg属性。不能通过TS语法检测;而 unknown 类型的值也不能将值赋给 any 和 unknown 之外的类型变量
总结
any 和 unknown 都是顶级类型,但是 unknown 更加严格,不像 any 那样不做类型检查,反而 unknown 因为未知性质,不允许访问属性,不允许赋值给其他有明确类型的变量。unknown 不破坏其他类型
15.如何将 unknown 类型指定为一个更具体的类型?
- 使用 typeof 进行类型判断(这些缩小类型范围的技术都有助于TS基于控制流程下的类型分析)
1 function unknownToString(value: unknown): string {
2 if (typeof value === "string") {
3 return value;
4 }
5
6 return String(value);
7 }
- 对 unknown 类型使用类型断言
要强制编译器信任类型为 unknown 的值为给定类型,则可以使用类型断言:
1 const value: unknown = "Hello World";
2 const foo: string = value; // Error
3 const bar: string = value as string; // OK
断言错了时语法能通过检测,但是运行的时候就会报错了!
1 const value: unknown = "Hello World";
2
3 const bar: number = value as number; // runtime Error
16.类型断言
在TypeScript中,类型断言(Type Assertion)可以用来告诉编译器某个值的确切类型。它类似于其他编程语言中的类型转换,但在运行时不会对数据进行任何转换或检查。
在TypeScript中,有两种形式的类型断言:
- 尖括号语法(Angle Bracket Syntax):
let someValue: any = "hello world";
let strLength: number = (<string>someValue).length;
在上面的例子中,我们使用尖括号语法将someValue
断言为string
类型,并赋给strLength
变量。这样,在编译时就能通过类型检查,可以安全地获取字符串长度。
- as 语法(as Syntax):
let someValue: any = "hello world";
let strLength: number = (someValue as string).length;
在上面的例子中,我们使用as
关键字将someValue
断言为string
类型,并赋给strLength
变量。这种形式的类型断言更加常用,也更加推荐使用。
需要注意的是,类型断言只是在编译时起作用,在运行时并不会影响实际的对象。因此,类型断言要谨慎使用,确保进行类型断言的值与断言的类型是兼容的,否则可能导致运行时错误。
例如,如果对一个数字类型的变量进行字符串类型的断言,可能会导致类型不匹配的错误:
let num: number = 123;
let str: string = num as string; // 错误,无法将数字类型断言为字符串类型
总结一下,类型断言是一种告诉编译器某个值的确切类型的方式,在TypeScript中可以使用尖括号语法或as语法来实现。但需要注意的是,类型断言只在编译时起作用,并不能改变运行时的实际类型。
17.void和never区别
在TypeScript中,void
和never
是两个不同的类型,用于表示函数返回值或表达式的特性。
void
类型表示函数没有返回值,或者说函数返回的值为undefined。常见的使用场景是在函数声明或函数定义时明确指定函数的返回类型为void
。
function greet(): void {
console.log("Hello!");
}
在上述示例中,函数greet()
被标注为返回类型为void
,因此该函数没有返回值。
- never`类型表示函数永远不会正常结束或返回结果,或者抛出异常。它通常用在以下情况:
- 函数总是会抛出错误或产生无法到达的终点。
- 函数包含无限循环,不会停止执行。
function throwError(message: string): never {
throw new Error(message);
}
function infiniteLoop(): never {
while (true) {
// 无限循环
}
}
在上述示例中,函数throwError
会抛出一个错误,并且永远不会正常返回。函数infiniteLoop
则是一个无限循环,因此也永远不会正常返回。
总结一下,void
类型用于表示没有返回值的函数,而never
类型用于表示永远不会正常返回的函数或表达式。void
标志着函数结束后会得到undefined,而never
标志着函数永远不会返回
18.函数如何声明数据类型
在TypeScript中,可以通过以下几种方式来声明函数的数据类型:
- 使用函数类型标注(Function Type Annotation):通过在函数名后面使用冒号(:)来指定函数的参数类型和返回值类型。
function add(x: number, y: number): number {
return x + y;
}
在上述示例中,函数add
被标注为接受两个参数 x
和 y
,它们都是数字类型,并且返回值也是数字类型。
- 使用变量类型推断(Variable Type Inference):如果没有显式指定函数的数据类型,TypeScript 可以根据赋值表达式自动推断出函数的数据类型。
const multiply = (a: number, b: number) => {
return a * b;
};
在上述示例中,函数 multiply
的类型会被自动推断为接受两个数字类型的参数,并返回一个数字类型的结果。
- 使用接口(Interface)定义函数类型:可以使用接口来描述函数的形状,其中包含函数参数和返回值的类型定义。
interface MathOperation {
(x: number, y: number): number;
}
const subtract: MathOperation = (a, b) => {
return a - b;
};
在上述示例中,我们定义了一个接口 MathOperation
,它描述了一个接受两个数字类型参数并返回一个数字类型的函数。然后,我们将函数 subtract
的类型指定为 MathOperation
,确保它符合该接口的定义。
无论使用哪种方式,明确指定函数的参数类型和返回值类型可以提供更好的类型安全性,并且能够在编译时捕获潜在的类型错误。
19.tscongfig.json作用
sconfig.json是一个用于配置TypeScript编译器的配置文件。该文件用于指定项目中的TypeScript编译选项,以控制编译器的行为和输出结果。
以下是tsconfig.json文件的作用:
- 指定编译选项:可以在tsconfig.json中配置各种编译选项,如目标版本(target)、模块系统(module)、输出目录(outDir)、源文件列表(files、include、exclude)等。通过这些选项,可以自定义编译器的行为,以满足项目需求。
- 代码检查和语言特性配置:TypeScript编译器可以根据配置文件中的选项对代码进行类型检查,并提供语言特性的支持。通过配置文件,可以启用或禁用某些语言特性,设置严格的类型检查级别,处理特定的JavaScript模式等。
- 引入第三方库的声明文件:tsconfig.json可以配置使用引入的第三方库的声明文件(type definitions)。通过配置"types"或"typeRoots"选项,可以告诉编译器在编译过程中使用相应的声明文件,以获得更好的类型检查和编辑器支持。
- 项目整合和管理:通过使用tsconfig.json,可以将整个项目的配置统一管理。开发者可以根据需要创建多个不同的tsconfig.json文件,从而实现项目的模块化和扩展。
- IDE集成和开发工具支持:大多数IDE和代码编辑器都会自动检测项目中的tsconfig.json文件,并根据其配置提供相关的代码提示、错误检查、重构等功能。tsconfig.json文件可以增强开发者在IDE中的开发体验。
总之,tsconfig.json文件是TypeScript项目中的关键配置文件,用于控制编译器的行为、配置项目选项以及提供开发工具和IDE的支持。通过合理配置tsconfig.json,可以更好地管理和组织TypeScript项目。
20.如何检测null和undefined
在 TypeScript 中,可以使用类型断言、严格模式和条件语句来检测 null
和 undefined
。
- 类型断言:使用类型断言来告诉编译器变量的实际类型,并明确排除
null
和undefined
。
typescript复制代码let value: string | null = "Hello";
let length: number = (value as string).length;
- 严格模式:通过启用 TypeScript 的严格模式(strictNullChecks),编译器会强制检查
null
和undefined
的赋值情况。
// tsconfig.json
{
"compilerOptions": {
"strictNullChecks": true
}
}
let value: string | null = "Hello";
let length: number = value.length; // 编译错误,提示可能为null
- 条件语句:使用条件语句判断变量是否是
null
或undefined
。
let value: string | null = "Hello";
if (value !== null && value !== undefined) {
let length: number = value.length;
}
- 可选链操作符(Optional Chaining):在 TypeScript 3.7+ 版本中,可使用可选链操作符
?.
来安全地访问可能为null
或undefined
的属性或方法。
let obj: { prop?: string } = {};
let length: number | undefined = obj.prop?.length;
这些方法可以帮助您在 TypeScript 中检测和处理 null
和 undefined
值,以确保代码的安全性和可靠性。使用合适的方法可以避免潜在的空指针异常和未定义的行为
21.非空断言
非空断言(Non-null Assertion)是 TypeScript 中的一种语法,用于告诉编译器一个表达式不会为 null
或 undefined
。
在 TypeScript 中,当我们使用 !
在一个可能为 null
或 undefined
的表达式后面时,就表示我们断言该表达式不会为 null
或 undefined
。
下面是使用非空断言的示例:
let value: string | null = "Hello";
let length: number = value!.length; // 使用非空断言
console.log(length); // 输出:5
在上述示例中,我们在表达式 value.length
后添加了 !
,即 value!.length
,这表示我们断言 value
不会为 null
或 undefined
,因此可以安全地访问其 length
属性。
需要注意的是,使用非空断言时要确保自己对代码的控制,以避免出现潜在的空指针异常。如果在使用非空断言时误判了表达式的类型,或者在运行时出现了 null
或 undefined
值,依然可能导致错误。
非空断言应该谨慎使用,并且建议在能够使用其他类型检查机制(如严格模式、条件语句等)的情况下,尽量避免使用非空断言来提高代码的健壮性和安全性。
22.ts对象混入
在 TypeScript 中,对象混入(Object Mixing)是一种将多个对象合并成一个对象的技术。它常用于实现对象的复用和组合。
可以通过以下几种方式来实现对象混入:
- 手动属性赋值:使用对象的属性赋值操作,逐个将多个对象的属性复制到一个新的目标对象中。
function mixin(target: any, ...sources: any[]): void {
for (const source of sources) {
for (const key in source) {
if (source.hasOwnProperty(key)) {
target[key] = source[key];
}
}
}
}
const objectA = { foo: 1 };
const objectB = { bar: 2 };
const mergedObject = {};
mixin(mergedObject, objectA, objectB);
console.log(mergedObject); // 输出:{ foo: 1, bar: 2 }
Object.assign()
方法:使用Object.assign()
方法将多个源对象的属性合并到目标对象中。
const objectA = { foo: 1 };
const objectB = { bar: 2 };
const mergedObject = Object.assign({}, objectA, objectB);
console.log(mergedObject); // 输出:{ foo: 1, bar: 2 }
- 类继承和 Mixin 类:使用类继承和 Mixin 类来实现对象混入,从而实现对类的方法和属性的复用和组合。
class MyMixin {
mixMethod() {
console.log("Mixin method");
}
}
class MyClass extends MyMixin {
myMethod() {
console.log("My class method");
}
}
const myObject = new MyClass();
myObject.myMethod(); // 输出:My class method
myObject.mixMethod(); // 输出:Mixin method
l以上是几种常见的对象混入方式。根据实际需求和场景,您可以选择适合的方式进行对象的属性和方法复用、组合和扩展
23.type和interface区别
在许多编程语言中,包括Java和TypeScript等,type
和interface
是用于定义自定义数据类型的关键字。它们在某些方面有相似之处,但也有一些区别。
以下是type
和interface
之间的区别:
- 语法:
type
使用type
关键字来定义,而interface
使用interface
关键字来定义。 - 声明方式:使用
type
可以声明多种形式的类型,包括基本类型、联合类型、交叉类型、函数类型等。而interface
主要用于声明对象类型和类的结构。 - 可扩展性:
interface
支持扩展,可以通过继承其他接口来添加或修改属性和方法。而type
不支持直接的扩展机制。 - 其他语法特性:
interface
可以声明可选属性、只读属性和索引签名等特性,这些特性对于定义对象类型非常有用。而type
可以使用typeof
操作符获取类型的元数据,并且可以使用as
关键字进行类型断言。 - 使用场景:
interface
通常用于描述对象的结构,例如定义一个接口来表示用户、产品或API响应的数据结构。而type
适用于更复杂的类型定义,例如联合类型、交叉类型和函数类型的组合。
总体而言,interface
更加专注于对象类型的定义和扩展,而type
则更加灵活,可以用于各种类型的定义。选择使用哪种关键字取决于你遇到的具体需求和上下文。在某些情况下,它们可以互换使用,但在其他情况下,则可能更适合使用其中之一。
24.ts变量及变量提升
在TypeScript中,变量声明与JavaScript类似,并且遵循JavaScript的变量提升机制。
变量声明包括两种关键字:var
和let
(还有const
用于声明常量)。
-
var
关键字: 使用var
声明的变量存在变量提升,即它们可以在声明之前被访问到。typescript复制代码console.log(name); // 输出:undefined var name = "John"; console.log(name); // 输出:"John"
在上述示例中,虽然变量
name
在声明之前被访问,但其值为undefined
。这是由于变量提升,使得变量的声明在代码执行之前就会被解析。 -
let
关键字: 使用let
声明的变量也存在变量提升,但在声明之前访问会抛出错误。typescript复制代码console.log(age); // 抛出 ReferenceError 错误 let age = 25; console.log(age); // 输出:25
在上述示例中,尝试在变量
age
声明之前访问它会导致ReferenceError
错误。与var
不同,使用let
声明的变量在声明之前是不可访问的。
值得注意的是,var
存在一些作用域问题,会存在变量提升并且具有函数作用域(函数内部可访问),而let
和const
具有块级作用域(只在所在代码块内可访问),并且不存在变量提升。
尽管TypeScript允许使用var
、let
和const
关键字声明变量,但为了遵循最佳实践并减少错误,推荐使用let
和const
来声明变量,并根据需要选择合适的作用域。
25.ts装饰器
在TypeScript中,装饰器(Decorators)是一种特殊的声明,用于修改类、方法、属性或参数的行为。装饰器可以附加元数据、修改类的定义、替换类的方法或属性。
装饰器使用@
符号紧跟在被修饰的目标之前,并可以接收一些参数。它们可以应用于类、类中的方法、类的属性以及类方法的参数。
下面是一些常见的装饰器用法:
-
类装饰器:
function MyDecorator(target: Function) { // 在此处可以修改类的行为或添加附加元数据 } @MyDecorator class MyClass { // 类定义 }
类装饰器在类声明之前对类进行修饰。它接收一个参数,即被修饰的类的构造函数。在装饰器内部,可以对类的行为进行修改或添加附加元数据。
-
方法装饰器:
class MyClass { @MyDecorator myMethod() { // 方法定义 } }
方法装饰器应用于类方法,并可以修改方法的行为、添加附加元数据或替换方法的实现。它接收三个参数:被修饰的类的原型、方法的名称和方法的属性描述符。
-
属性装饰器:
class MyClass { @MyDecorator myProperty: string; }
属性装饰器应用于类的属性,并允许修改属性的行为或添加附加元数据。它接收两个参数:被修饰的类的原型和属性的名称。
-
参数装饰器:
class MyClass { myMethod(@MyDecorator param: string) { // 方法定义 } }
参数装饰器应用于方法的参数,并可以修改参数的行为或添加附加元数据。它接收三个参数:被修饰的类的原型、方法的名称和参数的索引。
装饰器可以应用多个,多个装饰器按照从上到下的顺序进行求值和应用。
26.ts混入
在TypeScript中,混入(Mixin)是一种将多个类的功能组合到一个类中的技术。它允许一个类通过复用其他类的方法和属性来获取额外的功能。
使用混入可以避免类之间的继承层次结构过于复杂或产生重复代码的情况。它提供了一种更灵活和可复用的方式来组合不同类的功能。
要实现混入,在TypeScript中可以使用以下几种方式:
-
类继承混入:
class Animal { eat(): void { console.log("Animal is eating"); } } class Flyable { fly(): void { console.log("Flying..."); } } // 使用 extends 关键字将 Animal 和 Flyable 混入到 Bird 中 class Bird extends Animal implements Flyable {} const bird = new Bird(); bird.eat(); // 输出:Animal is eating bird.fly(); // 输出:Flying...
在上面的示例中,
Bird
类通过extends
关键字同时继承了Animal
类,并使用implements
关键字实现了Flyable
接口。这样,Bird
类就具有了Animal
类和Flyable
接口中定义的方法。 -
对象复制混入:
class Animal { eat(): void { console.log("Animal is eating"); } } class Flyable { fly(): void { console.log("Flying..."); } } function mixin(target: any, ...sources: any[]): void { Object.assign(target, ...sources); } const bird: any = {}; mixin(bird, new Animal(), new Flyable()); bird.eat(); // 输出:Animal is eating bird.fly(); // 输出:Flying...
在上述示例中,通过定义一个
mixin
函数,在该函数内部使用Object.assign
方法将多个对象的属性和方法复制到目标对象中。通过调用mixin
函数将Animal
类和Flyable
类的实例复制到bird
对象上,从而实现混入的效果。
无论是类继承混入还是对象复制混入,混入都可以在一个类中组合多个类或对象,从而获得它们的功能。这种方式可以让代码更加模块化、可维护,并促进代码重用。
27.封装map方法
要封装 map
方法,你可以使用泛型和函数类型来定义一个通用的 map
函数。下面是一个示例:
function map<T, U>(arr: T[], callback: (value: T, index: number, array: T[]) => U): U[] {
const result: U[] = [];
for (let i = 0; i < arr.length; i++) {
result.push(callback(arr[i], i, arr));
}
return result;
}
// 使用示例
const numbers = [1, 2, 3, 4];
const doubled = map(numbers, (value) => value * 2);
console.log(doubled); // [2, 4, 6, 8]
在上面的示例中,map
函数接受一个数组和一个回调函数作为参数。回调函数会被应用到数组的每个元素上,并返回一个新的数组。map
函数使用泛型 T
和 U
来表示输入数组的类型和输出数组的类型。回调函数的类型是 (value: T, index: number, array: T[]) => U
,其中 value
是当前元素的值,index
是当前元素的索引,array
是原始数组。函数体内部使用一个循环遍历数组,并将回调函数的结果添加到结果数组中,最后返回结果数组。
你可以根据需要修改回调函数的实现,以适应不同的需求。