文章目录
- 前言😏
- 以下是关于 TypeScript 的一些偏僻的面试题😝
- 1. 泛型约束中的 "extends" 关键字有哪些用法和含义?
- 2. 什么是交叉类型(Intersection Types)?如何在 TypeScript 中定义和使用它们?
- 3. 请解释 "keyof" 关键字的含义和用法。
- 4. TypeScript 中的 "never" 类型有什么用途?请给出一个示例。
- 5. 如何定义一个函数类型的 "this" 参数,以及在函数中使用它?
- 6. 如果有一个类型是由两个类型 "A" 和 "B" 组成的,并且 "A" 和 "B" 中都有相同的属性名,那么 TypeScript 会怎么处理?
- 7. 如果有一个变量的类型是 "unknown",那么我们在使用这个变量时需要注意什么?
- 8. TypeScript 中有哪些用于描述`函数形参`、`返回值`和`异常的装饰器`?
- 9. `TypeScript` 中在类型声明中使用 "`import`" 语句,这有什么作用?
- 10. 如果两个模块的接口名称相同,但结构不同,那么如何在 TypeScript 中合并这两个接口?
前言😏
笔者曾经被问道:TypeScript 中 type 和 interface 区别
我们看看AI的答案:
在 TypeScript 中,type
和 interface
都可以用来定义类型。它们的主要区别在于:type
可以定义任何类型,而 interface
只能定义对象类型。
具体来说,type
可以定义原始类型、联合类型、元组类型和函数类型等,而 interface
只能定义对象类型、类类型和函数类型。
下面是一些具体的区别:
type
可以定义原始类型、联合类型、元组类型和函数类型,而interface
只能定义对象类型、类类型和函数类型。
type MyType = boolean | number; // 原始类型和联合类型
interface MyInterface {
x: number;
y: number;
} // 对象类型
type Point = [number, number]; // 元组类型
interface Shape {
color: string;
width: number;
height: number;
} // 对象类型
type HelloFunction = (name: string) => string; // 函数类型
interface GoodbyeFunction {
(name: string): string;
} // 函数类型
type
支持 extends 和交叉类型,而interface
只支持 extends。
type Base = { x: number };
type Derived = Base & { y: number };
type Merged = Base | { z: number };
interface Base {
x: number;
}
interface Derived extends Base {
y: number;
}
interface
支持定义合并,即多个同名接口的成员会合并为同一个接口,而type
不支持。
interface Foo {
x: number;
}
interface Foo {
y: string;
}
const obj: Foo = { x: 1, y: "hello" };
总之,type
和 interface
都是用于定义类型的关键字,它们的主要区别在于 type
可以定义任何类型,而 interface
只能定义对象类型。
在选择使用哪种关键字时,应根据需要来确定使用范围和功能,以提高代码的可读性和可维护性。
😊以上就是AI的回答啦~
那么,对于没有深入研究且在实际开发中没有使用到 TypeScript 的我,在回答那个问题的时候自然是回答不上的,现在有了AI的加持,可以多学习了,不仅问了下AI上面那个问题的答案,并且让AI列举了十道关于TypeScript那些偏僻的面试题
以下是关于 TypeScript 的一些偏僻的面试题😝
1. 泛型约束中的 “extends” 关键字有哪些用法和含义?
在 TypeScript 中,“extends” 关键字在泛型约束中使用,用来指定泛型类型的类型范围和约束条件,可以用于:
1. 类型的范围约束
在泛型定义中,使用 “extends” 关键字,指定一个类型约束范围,确保泛型所接收的类型必须是一个指定的类型。
例如:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
loggingIdentity('123'); // 输出 3
上面的例子中,“T extends Lengthwise” 说明泛型变量 “T” 必须满足 “Lengthwise” 接口的约束(即具有 length 属性)。
2. 多重约束
可以针对一个泛型变量指定多个约束条件。
例如:
function copyFields<T extends U, U>(target: T, source: U): T {
for (let id in source) {
target[id] = (<T>source)[id];
}
return target;
}
let x = { a:1, b: 2, c: 3, d: 4 };
copyFields(x, { b: 10, d: 20 }); //这个是可以的.
copyFields(x, { e: 10 }); // error,因为 source 上不存在 a 或 b 属性
上面的例子中,“T extends U” 说明泛型变量 “T” 必须满足 “U” 类型的约束,从而确定了泛型变量 “T” 的类型范围。
总之,“extends” 关键字用于定义泛型类型的约束条件,可以指定类型或接口,也可以定义多重约束。在使用泛型的时候,可以通过对泛型变量进行约束,确保所传入的参数符合预期类型。
2. 什么是交叉类型(Intersection Types)?如何在 TypeScript 中定义和使用它们?
交叉类型(Intersection Types)是 TypeScript 中表示多个类型的交集的一种方式。
简单地说,它可以将多个类型合并成一个类型。例如,假设有两个类型 A 和 B:
interface A {
propA: string;
}
interface B {
propB: number;
}
我们可以使用交叉类型将这两个类型合并成一个类型,定义一个新的类型 C:
type C = A & B;
交叉类型 C
包含了类型 A
和 B
中的所有属性。因此,变量 c
可以包含 propA
和 propB
两个属性:
const c: C = { propA: "hello", propB: 123 };
交叉类型也可以使用于继承:
class D implements A, B {
propA = "hello";
propB = 123;
}
在使用交叉类型时,需要注意以下几点:
- 只有需要合并类型的属性之间类型冲突时才需要使用交叉类型,否则使用普通类型即可。
- 交叉类型中所有类型的属性都必须满足相同的访问限制(public、private 或 protected)。
Union
类型和Intersection
类型的区别在于,Union
类型表示多个类型中的任意一个,而交叉类型表示多个类型的交集。
3. 请解释 “keyof” 关键字的含义和用法。
“keyof” 关键字是 TypeScript
中用来获取一个类型(通常是对象类型)所有键(即属性名)的名称的一种方式。
举个例子,假设有一个对象类型:
type Person = {
name: string;
age: number;
gender: "male" | "female";
};
我们可以使用 “keyof” 关键字获取这个对象类型的所有键的名称:
type PersonKeys = keyof Person;
// 等价于 type PersonKeys = "name" | "age" | "gender"
这意味着我们可以使用 PersonKeys
来定义一个函数的参数类型:
function getProp(obj: Person, key: keyof Person): any {
return obj[key];
}
在上面的例子中,参数 key 的类型指定为 “keyof Person”,即表示参数 key 必须为 Person
中定义过的属性名,而不是其他任何字符串。
此外,“keyof” 关键字还可以用于泛型类型或条件类型:
type PersonPropTypes<T> = {
[K in keyof T]: T[K] extends string ? K : never;
};
在上面的例子中,我们使用了泛型 “T” 和 “keyof T”。“keyof T” 表示泛型 T 中所有属性名的联合类型,我们可以通过它来定义一个新的对象类型,该对象类型只包含泛型 T 中值为字符串类型的属性名。
总之,“keyof” 关键字是一种非常有用的 TypeScript
特性,它常用于为对象类型中的属性命名并且在编写通用代码时提供了更好的类型检查。
4. TypeScript 中的 “never” 类型有什么用途?请给出一个示例。
在 TypeScript 中,never
类型表示一个永远不会发生的值的类型。在实际应用中,never
类型常常用在联合类型或泛型中:
1. 当函数的返回类型为 never
时,表示该函数永远不会正常返回。
function throwError(message: string): never {
throw new Error(message);
}
2. 在泛型中使用 never
类型可以帮助我们确保某些类型不会被传递进来,进而让编译器帮我们避免一些可能出现的错误。
type NonNullable<T> = T extends null | undefined ? never : T;
type MyRequired<T> = {
[P in keyof T]-?: NonNullable<T[P]>;
};
上面的代码定义了一个泛型 NonNullable
和一个接口 MyRequired
。其中,NonNullable
可以将 null
和 undefined
类型转化为 never
类型,而 MyRequired
可以将每个属性变为必需的,并且确保不会出现 null
或 undefined
。
总的来说,never
类型的主要用途是辅助开发人员进行类型检查和编写更加安全的代码。
5. 如何定义一个函数类型的 “this” 参数,以及在函数中使用它?
在 TypeScript
中可以使用 this
参数来显式地指定函数内部的 this
指向。在函数类型上,我们可以使用 this
参数来描述该函数期望的上下文对象类型。
具体的用法如下:
// 定义一个接口来描述需要使用 this 参数的函数类型
interface MyFunc {
(this: Window, event: Event): void;
}
// 定义一个对象,在该对象上调用函数时需要使用 this 参数来指定上下文对象
const myObj = {
handleClick: function(this: Window, event: Event) {
console.log("event: ", event);
console.log("this: ", this);
}
};
// 在对象上调用函数时需要使用 call 或 apply 方法来指定上下文对象
myObj.handleClick.call(window, new Event("click"));
上面的代码定义了一个函数类型 MyFunc
,它期望 this
指向 Window
对象。同时,我们也定义了一个对象 myObj
,并在其上面定义了一个函数 handleClick
,需要使用 this
参数来指定上下文对象。最后,在调用 myObj.handleClick
时,我们使用 call
方法来显式地指定 this
指向 window
对象。
需要注意的是,使用 this
参数需要显式地指定上下文对象的类型,这样可以在编译期间发现一些潜在的问题,让代码更加健壮。
6. 如果有一个类型是由两个类型 “A” 和 “B” 组成的,并且 “A” 和 “B” 中都有相同的属性名,那么 TypeScript 会怎么处理?
如果一个类型是由两个类型 “A” 和 “B” 组成的,并且 “A” 和 “B” 中都有相同的属性名,则 TypeScript 会合并这两个类型,并且将属性的类型合并为联合类型。
具体来说,TypeScript 会遵循以下规则:
-
对于非函数成员,如果两个类型中成员的类型相同,则最终类型中该成员的类型为这个相同的类型;如果两个类型中成员的类型不同,则最终类型中该成员的类型为两个成员的类型的联合类型。
-
对于函数成员,如果两个类型中成员的类型都为函数,则最终类型中该成员的类型为函数的重载列表。如果两个类型中函数成员的重载列表性质不同,则会出现编译时错误。
下面是一个示例,它展示了当类型 “A” 和类型 “B” 中都有一个属性 “name” 时,TypeScript 是如何合并它们的:
type A = {
id: number;
name: string | null;
};
type B = {
name: string;
age: number;
};
type C = A & B;
const c: C = {
id: 1,
name: "Tom",
age: 18
};
在上面的代码中,我们定义了两个类型 “A” 和 “B”,它们都有一个属性 “name”。然后,我们定义了一个类型 “C”,使用交叉类型将类型 “A” 和 “B” 合并在一起。最后,我们声明了一个变量 c
,它的类型为 “C”,并且给它赋值了一个包含 “id”、“name” 和 “age” 属性的对象。
在上面的代码中,属性 “name” 的类型为 string | null
和 string
,TypeScript 会将它们的类型合并为 string | null
类型。因此,类型 “C” 中属性 “name” 的类型为 string | null
。
总之,当一个类型由两个类型合并而成时,TypeScript 遵循一定的规则来进行合并,以便编译器能够正确地推断类型,从而提高代码的可靠性。
7. 如果有一个变量的类型是 “unknown”,那么我们在使用这个变量时需要注意什么?
8. TypeScript 中有哪些用于描述函数形参
、返回值
和异常的装饰器
?
如果一个变量的类型是 unknown
,那么在使用这个变量时需要注意它的类型是未知的,因此我们需要进行类型检查或类型断言来避免类型错误。
具体来说,我们在使用
unknown
类型的变量时,需要遵循以下规则:
-
unknown
类型变量不能直接赋值给其他类型的变量,除了any
类型和unknown
类型本身之外。这是因为any
类型可以隐式转换为任何类型,而unknown
类型的变量可以接受任何类型的值,但我们必须显式地对其进行类型检查或类型断言才能使用它。 -
在对
unknown
类型的变量进行类型检查时,我们需要使用类型保护来确定其实际类型、属性和方法的可用性,以免在运行时出现类型错误。
下面是一个示例,它展示了如何使用类型检查或类型断言来使用 unknown
类型的变量:
function printLength(str: unknown) {
if (typeof str === "string") {
console.log(str.length); // OK
} else {
console.log("The type of str is unknown.");
}
}
function getValue(obj: { [key: string]: unknown }, key: string) {
if (typeof obj[key] === "string") {
console.log(obj[key].toUpperCase()); // OK
} else {
console.log("The type of obj[key] is unknown.");
}
}
let x: unknown = "hello";
let y: string;
let z: number;
// y = x; // 报错,无法将类型“unknown”分配给类型“string”
y = typeof x === "string" ? x : ""; // OK,使用类型断言
// z = x; // 报错,无法将类型“unknown”分配给类型“number”
z = typeof x === "number" ? x : 0; // OK,使用类型断言
在上面的代码中,我们定义了一个函数 printLength
,它接受一个 unknown
类型的参数,并在函数内部使用类型检查来确定参数的实际类型,从而避免类型错误。另外,我们还定义了一个函数 getValue
,它接受一个对象和一个键名,用于获取该键对应的值,并在函数内部对该值进行类型检查。最后,我们声明了一个 unknown
类型的变量 x
,并使用类型检查和类型断言来使用该变量。
总之,在使用 unknown
类型变量时,我们需要特别注意其类型是未知的,需要进行类型检查或类型断言才能使用,以避免类型错误。
9. TypeScript
中在类型声明中使用 “import
” 语句,这有什么作用?
在 TypeScript 中,有许多用于描述函数形参、返回值和异常的装饰器,具体如下:
1. @Paramtypes(...types: Function[])
:用于描述函数的形参类型。
function MyFunc(@Paramtypes(Number, String) arg1: any) {
// ...
}
2. @ReturnType(type: Function)
:用于描述函数的返回值类型。
@ReturnType(Number)
function MyFunc() {
// ...
}
3. @Throws(...types: any[])
:用于描述函数可能抛出的异常类型。
function MyFunc() {
@Throws(Error)
function innerFunc() {
// ...
}
// ...
}
4. @Asserts(expression: string)
:用于描述函数的运行结果是否满足某个条件。
function MyFunc() {
@Asserts("result.length > 0")
function innerFunc(): string[] {
const result = ["a", "b", "c"];
return result;
}
// ...
}
这些装饰器都是 TypeScript
中的实验性特性,需要在 tsconfig.json
文件中启用 “experimentalDecorators
” 选项才能使用。
使用这些装饰器可以帮助我们更好地描述函数的类型信息,从而提高代码的可靠性和维护性。
10. 如果两个模块的接口名称相同,但结构不同,那么如何在 TypeScript 中合并这两个接口?
在 TypeScript 中,可以使用模块合并来合并两个模块中的接口,具体的方法如下:
假设我们有两个模块 Module1
和 Module2
,它们中都定义了名称为 SampleInterface
的接口,但是它们的结构不同。我们可以在同一文件中定义一个新的模块 ModuleCombined
,并将模块 Module1
和 Module2
合并到这个模块中,然后在模块 ModuleCombined
中定义一个同名的接口,这个接口的结构就是将模块 Module1
和 Module2
中的同名接口的成员合并起来的结果。
下面是示例代码:
// module1.ts
export interface SampleInterface {
id: number;
name: string;
}
// module2.ts
export interface SampleInterface {
age: number;
address: string;
}
// moduleCombined.ts
import { SampleInterface as Interface1 } from './module1';
import { SampleInterface as Interface2 } from './module2';
interface SampleInterface extends Interface1, Interface2 {}
export { SampleInterface };
在上面的代码中,我们分别定义了两个模块 Module1
和 Module2
,它们中都定义了名称为 SampleInterface
的接口,但接口的结构不同。然后我们创建了一个新的模块 ModuleCombined
,并将模块 Module1
和 Module2
合并到这个模块中,最后在模块 ModuleCombined
中定义了同名的接口 SampleInterface
,这个接口继承了模块 Module1
和 Module2
中的同名接口,从而将两个接口中的成员合并起来。
这样,我们就成功地合并了模块 Module1
和 Module2
中的接口,可以通过 ModuleCombined.SampleInterface
来使用这个接口。注意,我们通过 import { SampleInterface as Interface1 } from './module1';
和 import { SampleInterface as Interface2 } from './module2';
来避免名称冲突。