一、对象
1.1 对象定义
// 匿名方式定义对象。
let a: { name: string; age: number } = { name: "xiaoming", age: 18 };
// 接口命名方式定义对象。
interface B {
name: string;
age: number;
}
let b: B = { name: "xiaoming", age: 18 };
// 类型别名方式定义对象。
type C = { name: string; age: number };
let c: C = { name: "xiaoming", age: 18 };
1.2 可选属性(?)
// 在属性名与冒号之间添加一个问号。
interface ABC {
x: number;
y?: number;
z?: number;
}
let a: ABC = { x: 1 };
/*
若使用了可选属性,在函数中作为参数时,
要么在函数中用if等判断哪些属性没传值,
要么使用解构在参数中设置默认值。
*/
1.3 只读属性(readonly)
1.3.1 基础语法
// 在属性名前添加关键词“readonly”。
interface ABC {
id: number;
readonly name: string;
readonly address: {
a1: string;
a2: string;
};
}
function test(abc: ABC) {
console.log(abc.id, abc.name, abc.address.a1, abc.address.a2);
abc.id = 1;
abc.name = "abc"; // 无法为“name”赋值,因为它是只读属性。
abc.address.a1 = "abc"; // 正确。
abc.address.a2 = "abc"; // 正确。
abc.address = { // 无法为“address”赋值,因为它是只读属性。
a1: "abc",
a2: "abc",
};
}
interface Person {
name: string;
age: number;
}
interface ReadonlyPerson {
readonly name: string;
readonly age: number;
}
let p1: Person = { name: "Jim", age: 20 };
// ReadonlyPerson兼容Person。
let p2: ReadonlyPerson = p1;
p2.name = "Tom"; // 无法为“name”赋值,因为它是只读属性。
p2.age = 21; // 无法为“age”赋值,因为它是只读属性。
p1.name = "Jimmy";
p1.age = 22;
// p1更改后,p2也会改变。
console.log(p1, p2);
// { name: 'Jimmy', age: 22 } { name: 'Jimmy', age: 22 }。
1.3.2 详细介绍
错误理解:readonly只能用于数组和元组字面量类型(许多文档中关于readonly的介绍)。 正确理解:readonly既可以用于类型定义中的属性(如interface/class),也可以用于类型注解中的数组/元组(如readonly string[]) —— 这是两种不同但相关的用法。
readonly的两种主要用法:
在接口/类中修饰属性。
interface ABC {
id: number;
readonly name: string; // 合法:name属性只读。
readonly address: { // 合法:address属性只读。
a1: string;
a2: string;
};
}
-
作用:表示该属性在对象创建后不能被重新赋值。
-
适用范围:interface、type、class中的属性。
-
这是TypeScript从1.x就支持的特性。
示例:
const obj: ABC = {
id: 1,
name: "Alice",
address: { a1: "Beijing", a2: "China" }
};
obj.name = "Bob"; // 错误!Cannot assign to 'name' because it is a read-only property.
obj.address = {}; // 错误!address是只读属性
obj.address.a1 = "Shanghai"; // 允许!因为address对象本身不是readonly。
// 注意:`readonly address`表示`address`引用不可变,但`address`内部的对象属性默认仍可修改(除非内部也加`readonly`)。
在类型注解中修饰数组/元组(“字面量”用法)。
let arr: readonly number[] = [1, 2, 3]; // 只读数组。
let tuple: readonly [string, number] = ["a", 1]; // 只读元组。
-
作用:禁止调用push、splice等mutating方法,也禁止通过索引赋值(如arr[0] = 10)。
-
这是TypeScript 3.4引入的readonly类型修饰符,用于简化ReadonlyArray<T>的写法。
为什么很多文档会有“只能用于数组/元组”的介绍?
可能是因为TypeScript官方文档在介绍readonly类型修饰符(readonly type modifier)时,重点强调了它在数组/元组上的新用法:
- “TypeScript 3.4 introduced a new syntax for ReadonlyArray<T>: readonly T[].”
但这并不意味着旧的readonly属性修饰符被废弃或限制!
实际上,两者共存且用途不同:
| 属性只读 | readonly prop: T。 | interface / class / type中的属性。 | TS 1.0+ |
| 数组/元组只读 | readonly T[]或readonly [T, U]。 | 类型注解(变量、参数、返回值等)。 | TS 3.4+ |
readonly属性 vs Readonly<T>:(你还可以用工具类型Readonly<T>让整个对象只读)。
interface User {
name: string;
age: number;
}
type ROUser = Readonly<User>;
// 等价于:
// {
// readonly name: string;
// readonly age: number;
// }
总结:
interface ABC {
id: number;
readonly name: string; // 正确:属性只读(TS 1.0+支持)。
readonly address: { … }; // 正确。
}
-
这不是“仅允许数组/元组”的例外,而是readonly的原始设计用途之一。
-
TypeScript中的readonly有两个上下文:
-
成员修饰符(用于interface/class属性)。
-
类型前缀修饰符(用于readonly T[])。
两者都是官方支持的标准特性,没有冲突,也不互相排斥。
1.4 索引签名
// 使用中括号将属性名括起来。
// 当我们不关心属性名,只关心其属性名称数据类型、属性值数据类型时,我们可以使用索引签名。
interface TestArray {
[index: number]: string;
}
const myArray1: TestArray = ["a", "b", "c"];
const myArray2: TestArray = [1, 2, 3];
// 报错:不能将类型“number”分配给类型“string”。
interface TestString {
[index: string]: number;
}
const myObject1: TestString = { a: 1, b: 2, c: 3 };
const myObject2: TestString = { 1: "a", 2: "b", 3: "c" };
// 报错:不能将类型“string”分配给类型“number”。
interface TestNumberAndString1 {
name1: string;
age1: number;
[index: number]: number;
name2: string;
age2: number;
}
const myObject3: TestNumberAndString1 = {
name1: "a",
age1: 1,
name2: "b",
age2: 2,
0: 1,
1: 2,
2: 3,
8: 4,
"9": 5, // 符合“[index: number]: number;”要求,会隐式类型转换。
a: 6, // 报错:不能将类型“string”分配给类型“number”。
};
interface TestNumberAndString2 {
name1: string; // 报错:类型“string”的属性“name1”不能赋给“string”索引类型“number”。
age1: number;
[index: number | string]: number;
name2: string; // 报错:类型“string”的属性“name2”不能赋给“string”索引类型“number”。
age2: number;
}
/*
“[index: number | string]: number;”
中的
“[index: number | string]”
包含了name1、name2、age1、age2,
但name1、name2的属性值(string)不符合其要求(number)。
*/
interface TestReadonly {
readonly [index: number]: string; // 只读索引签名。
}
1.5 扩展类型(继承)
// 使用extends实现继承。
interface A {
name: string;
}
interface B {
age: number;
}
interface Dog extends A, B {
breed: string;
}
1.6 交叉类型(&)
// 使用“&”符号连接多个接口。
interface Circle {
radius: number;
}
interface Colorful {
color: string;
}
type CircleAndColorful = Circle & Colorful;
const circleAndColorful: CircleAndColorful = {
radius: 1,
color: "red",
};
function abc(cc: Circle & Colorful) {
console.log(cc.radius);
console.log(cc.color);
}
二、泛型
2.1 泛型对象类型
interface Box<Type> {
content: Type;
}
let box: Box<string> = {
content: "hello world",
};
type Box<Type> = {
content: Type;
};
// 类型别名在泛型中的另一种用法(编写其它类型的通用的辅助类型)。
type OrNull<Type> = Type | null; // 该类型定义的变量可为"Type"数据类型,也可为"null"。
type OneOrMany<Type> = Type | Type[]; // 该类型定义的变量可为"Type"数据类型,也可为"Type"数据类型数组。
type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>; // "OrNull<OneOrMany<Type>>" => "OrNull<Type | Type[]>" => "Type | Type[] | null"。
/*
type OrNull<Type> = Type | null;
这是一种TypeScript中的泛型类型别名(Generic Type Alias)的用法。
这个类型别名的作用是:给任意类型"Type"添加"null"的可能性,从而表示“这个值可能是"Type"类型,也可能是"null"”。
使用示例:
type OrNull<T> = T | null;
let name: OrNull<string> = "Alice"; // OK。
name = null; // OK。
let age: OrNull<number> = 25; // OK。
age = null; // OK。
let user: OrNull<{ id: number }> = { id: 1 }; // OK。
user = null; // OK。
这种模式在以下场景中非常有用:
1、表示API返回的数据可能为空。
2、处理可选的中间状态(比如加载中尚未获取到数据)。
3、与后端或数据库交互时,字段可能为"null"。
类似内置工具类型:
TypeScript内置了类似的工具类型,比如:
1、NonNullable<T>:从T中移除"null"和"undefined"。
2、虽然没有内置 OrNull<T>,但你可以自己定义,如上所示。
总结:
type OrNull<Type> = Type | null;
是一个自定义的泛型类型别名,用于让任意类型"Type"可以为"null",提高代码的可读性和类型安全性。
*/
2.2 泛型类型
function identity<Type>(arg: Type): Type {
return arg;
}
// ………………………………………………………………………………….
// 函数类型表达式声明(函数类型语法)。
let myIdentity1 = identity;
let myIdentity2: <Type>(arg: Type) => Type = identity;
// myIdentity1和myIdentity2是等价的。
// 前者使用的是TS自动类型推断,后者使用的是显示声明变量类型(通过函数类型表达式,特别的是该函数类型表达式用了泛型)。
// ………………………………………………………………………………….
// 调用签名声明(对象字面量语法)。
let myIdentity3: { <Type>(arg: Type): Type } = identity;
interface GenericIdentityFn1 {
<Type>(arg: Type): Type;
}
let myIdentity4: GenericIdentityFn1 = identity;
console.log(myIdentity4(1)); // <1>(arg: 1) => 1。
console.log(myIdentity4<number>(1)); // <number>(arg: number) => number。
interface GenericIdentityFn2<Type> {
(arg: Type): Type;
}
let myIdentity5: GenericIdentityFn2<number> = identity;
console.log(myIdentity5(1)); // (arg: number) => number。
// 注意:当类型字面量只包含调用签名时,应该使用函数类型语法而不是对象字面量语法来定义函数类型。
2.3 泛型类
class GenericNumber<NumType> {
constructor(zero: NumType, add: (x: NumType, y: NumType) => NumType) {
this.zeroValue = zero;
this.add = add;
}
zeroValue: NumType;
add: (x: NumType, y: NumType) => NumType;
}
let myGenericNumber = new GenericNumber<number>(0, (x, y) => x + y);
console.log(myGenericNumber.zeroValue, myGenericNumber.add(1, 5)); // 0 6。
myGenericNumber.zeroValue = 8;
myGenericNumber.add = function (x, y) {
return x * y;
};
console.log(myGenericNumber.zeroValue, myGenericNumber.add(2, 5)); // 8 10。
2.4 泛型约束
2.4.1 基本语法
// 使用extends。
interface Lengthwise {
length: number;
}
// 限制Type类型必须包含length属性。
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
console.log(arg.length); // 3。
return arg;
}
console.log(loggingIdentity([1, 2, 3])); // [ 1, 2, 3 ]。
2.4.2 使用类型参数(keyof)
// 使用keyof关键词。
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key): Type[Key] {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
console.log(getProperty(x, "a")); // 1。
let y = [1, 2, 3, 4, 5, 6];
console.log(getProperty(y, 2)); // 3。
在TypeScript中,keyof是一个类型操作符(type operator),用于获取某个类型的所有公共属性名(键)组成的联合类型(union type)。
基本语法:
type Keys = keyof T;
-
T必须是一个对象类型(如interface、type、class等)。
-
keyof T的结果是:所有属性名的字符串字面量联合类型。
举例说明:
基础用法。
interface User {
id: number;
name: string;
email: string;
}
type UserKeys = keyof User;
// 等价于:
// type UserKeys = "id" | "name" | "email";
// ……………………………………………………………………………
interface Arrayish1 {
[n: number]: string;
}
interface Arrayish2 {
[n: string]: string;
// 注意:当索引签名为string类型时,实际上它是"string | number"的联合类型。详情如下解释。
}
type A = keyof Arrayish1; // 等价于:type UserKeys = number;
let a: A = 1; // 正确。
let b: A = "1"; // 报错:不能将类型“string”分配给类型“number”。
type B = keyof Arrayish2; // 等价于:type UserKeys = string | number;
let c: B = 1; // 正确。
let d: B = "1"; // 正确。
/*
当对象的索引签名定义为"[n: string]: string"时,用number类型的键(比如obj[0])去访问,
TypeScript却允许,并且把number键也视为合法 —— 甚至在某些上下文中,键的类型会被推断为"string | number"。
这背后的原因是:
JavaScript的对象属性名本质上都是字符串(或Symbol),
而TypeScript为了与JavaScript的运行时行为保持一致,对数字键做了特殊处理。
核心原因:
JavaScript中,obj[0]等价于obj["0"],也就是说,所有通过数字访问的对象属性,最终都会被转成字符串。
因此,TypeScript设计时决定:
如果一个对象有string索引签名([key: string]: T),那么它也隐式支持number作为键,因为number会被自动转为string。
索引签名为string,但可以用number访问。
如果你只定义了number索引签名,则不能用任意string访问。
即:string索引签名 => 兼容number键;number索引签名 ≠> 兼容任意string键。
为什么有时看到键的类型是"string | number"?
这通常出现在泛型约束或工具类型推导中。(eg:使用keyof获取索引签名对象的键类型)
为什么keyof Dict是"string | number"?
因为TypeScript认为:
你可以用任意string键访问(如dict["name"])。
也可以用任意number键访问(如dict[0] → 实际是dict["0"])。
所以keyof必须包含两者,以保证类型安全。
但这不意味着对象真的有number类型的属性,而是TypeScript在类型系统层面放宽了键的类型,以匹配JavaScript的运行时行为。
为什么索引签名为string,可以用number访问。但如果只定义了number索引签名,却不能用任意string访问?
"[key: number]: T"不会自动接受任意string键,因为并非所有字符串都能转换为有效数字。
而"[key: string]: T"接受number键,是因为所有数字都可以无损转为字符串(如0 → "0")。
这是TypeScript单向兼容的设计:只在安全、无歧义的方向上放宽类型限制。
*/
此时,UserKeys只能是"id"、"name"或"email"中的一个:
const key1: UserKeys = "name"; // 正确。
const key2: UserKeys = "age"; // 错误!"age"不在User的属性中。
与泛型结合:安全地访问对象属性(这是keyof最经典的用途)。
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Alice", email: "a@example.com" };
const name = getProperty(user, "name"); // 类型:string。
const id = getProperty(user, "id"); // 类型:number。
getProperty(user, "phone"); // 编译错误!
-
K extends keyof T:确保传入的key是T的合法属性。
-
返回类型T[K]:TypeScript能自动推导出对应属性的类型(索引访问类型)。
与typeof联用:获取具体对象的键。
const config = {
theme: "dark",
lang: "zh",
version: 2,
};
type ConfigKeys = keyof typeof config;
// 等价于:"theme" | "lang" | "version"。
注意:typeof config在类型上下文中表示“config的类型”,然后keyof提取其键。
在映射类型(Mapped Types)中使用。
keyof是实现工具类型(如Partial、Readonly)的基础。
// 自定义只读类型。
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
interface Todo {
title: string;
done: boolean;
}
type ReadonlyTodo = MyReadonly<Todo>;
// 相当于:
// {
// readonly title: string;
// readonly done: boolean;
// }
这里[K in keyof T]表示:遍历T的每一个键K,并为每个键生成一个属性。
注意事项:
只包含 public 属性。
keyof不会包含private或protected成员(因为它们在类型系统中不可见)。
对基本类型的行为。
对string、number等使用keyof会得到其原型方法名(一般不推荐这样用)。
type S = keyof string; // 很多方法名,如"charAt", "slice", …。
空对象类型。
type Empty = keyof {}; // never。
实际应用场景:
| 通用属性访问函数 | 如getProperty,避免硬编码属性名。 |
| 表单校验/动态字段 | 限制用户只能操作已知字段。 |
| API参数规范化 | 确保传入的字段名合法。 |
| 工具类型实现 | Partial<T>, Pick<T, K>, Omit<T, K>等都依赖keyof。 |
总结:
-
keyof T → 获取类型T的所有属性名组成的联合类型。
-
常与泛型、extends、T[K](索引访问类型)配合使用。
-
是TypeScript类型安全和元编程能力的核心之一。
-
编译后消失,零运行时开销。
2.5 使用类类型
// 工厂函数,专门创建类的(使用了构造签名)。
function create<Type>(c: { new (): Type }): Type {
return new c();
}
/*
"c: { new (): Type }"中只包含构造签名。
注意:当类型字面量只包含构造签名时,应该使用函数类型语法而不是对象字面量语法来定义函数类型。
*/
eg:
class Animal {
numLegs: number;
constructor(numLegs: number) {
this.numLegs = numLegs;
}
}
class Bird extends Animal {
keeper: Animal = new Animal(2);
}
class House {
numBedrooms: number;
constructor(numBedrooms: number) {
this.numBedrooms = numBedrooms;
}
}
// "new (v: number): T"要与类的构造函数参数匹配。
function create<T extends Animal>(c: { new (v: number): T }, val: number): T {
return new c(val);
}
console.log(create(Bird, 6).keeper.numLegs); // 输出2。因为Bird类中"keeper: Animal = new Animal(2);"。
2.6 typeof类型操作符
2.6.1 基础语法
function f() {
return {
a: 1,
b: 2,
};
}
type F = ReturnType<typeof f>;
/*
等价于:
type F = {
a: number;
b: number;
}
*/
/*
ReturnType<typeof f>是TS内置数据类型。其中f为函数(无返回值时,返回void)。
作用:获取函数类型的返回类型。
源码:type ReturnType<T extends (…args: any) => any> = T extends (…args: any) => infer R ? R : any;
有关"infer"关键词,详情请看2.8节。
*/
// ……………………………………………………………………………………….
let d = {
x: 1,
y: 2,
};
type D = typeof d;
/*
等价于:
type D = {
x: number;
y: number;
}
*/
// ……………………………………………………………………………………….
function abc() {}
type F = typeof abc;
// 等价于:type F = () => void。
// ……………………………………………………………………………………….
type A = 'a';
// 等价于:type A = 'a'。即变量类型为A时,只能为该变量赋值'a'。
// 区别于JS的typeof,TS中的typeof并不会只返回function、object。
2.6.2 索引访问类型
interface Abc1 {
name: string;
}
type A = Abc1["name"];
// 等价于:type A = string。
type Abc2 = {
name: string;
};
type B = Abc2["name"];
// 等价于:type B = string。
// ……………………………………..
type Abc3 = {
name: string;
age: number;
};
type C = Abc3["name" | "age"];
// 等价于:type C = string | number。
// ……………………………………..
const myArray = [
{ name: "Alice", age: 18 },
{ name: "Bob", isAdult: true },
{ name: "Eve", childName: ["Alice", "Bob"] },
];
type D = (typeof myArray)[number];
// 也可写成"typeof myArray[number];"。下面的也一样,可以把小括号去掉。
/*
等价于:
type D = {
name: string;
age: number;
isAdult?: never;
childName?: never;
} | {
name: string;
isAdult: boolean;
age?: never;
childName?: never;
} | {
name: string;
childName: string[];
age?: never;
isAdult?: never;
}
*/
type E = typeof myArray;
/*
等价于:
type E = ({
name: string;
age: number;
isAdult?: never;
childName?: never;
} | {
name: string;
isAdult: boolean;
age?: never;
childName?: never;
} | {
name: string;
childName: string[];
age?: never;
isAdult?: never;
})[]
*/
type F = (typeof myArray)[number]["age"];
/*
等价于:
type F = number | undefined。
*/
type G = (typeof myArray)[2]["isAdult"];
/*
等价于:
type G = boolean | undefined。
*/
type H = (typeof myArray)[2]["name"];
/*
等价于:
type H = string。
因为数组中的所有对象都有该属性且都为string类型。
*/
| T[string] | 获取对象所有string键对应的值类型(常用于索引签名)。 |
| T[number] | 获取数组/元组所有元素类型的联合。 |
| T[0] | 获取元组第一个元素的类型。 |
| T[keyof T] | 获取对象所有属性值的联合类型。 |
2.7 条件类型
2.7.1 基础语法
// 使用三元表达式。
interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
type Example1 = Dog extends Animal ? number : string;
// 等价于:type Example1 = number。
type Example2 = RegExp extends Animal ? number : string;
// 等价于:type Example2 = string。
eg:(可取代部分函数重载实例,简化函数)
interface Idlabel {
id: number;
}
interface Namelabel {
name: string;
}
// 函数重载,很麻烦,不宜阅读,且函数参数更多时,重载签名也会更多。
function createLabel1(id: number): Idlabel;
function createLabel1(name: string): Namelabel;
function createLabel1(nameOrId: string | number): Idlabel | Namelabel {
throw "unimplemented";
}
// 条件类型直接搞定。
type NameOrId<T extends number | string> = T extends number ? Idlabel : Namelabel;
function createLabel2<T extends number | string>(idOrName: T): NameOrId<T> {
throw "unimplemented";
}
2.7.2 条件类型约束
// 条件类型 + 泛型约束。
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
/*
也可写成:type MessageOf<T> = T extends { message: infer P } ? P : never;
有关"infer"关键词,详情请看2.8节。
*/
interface Email {
message: string;
}
interface Dog {
bark(): void;
}
type EmailMessageContents = MessageOf<Email>;
const email: EmailMessageContents = "hello";
type DogMessageContents = MessageOf<Dog>;
const dog: DogMessageContents = "woof";
// 报错:不能将类型“"woof"”分配给类型“never”。
2.8 infer类型操作符
在TypeScript中,infer是一个用于在条件类型(Conditional Types)中“推断”并捕获类型变量的关键字。它的核心作用是:从一个复杂类型中“提取”出你关心的部分,并将其绑定到一个类型参数上,供后续使用。
简单说:infer就像类型层面的“模式匹配 + 解构赋值”。
基本语法:
type MyType<T> = T extends (infer U) ? U : never;
-
infer U出现在条件类型的extends子句的右侧。
-
当T满足某种结构时,TypeScript会自动推断出U的具体类型。
-
推断出的U可以在true分支中使用。
核心用途:从类型结构中“提取”子类型。
提取函数的返回类型(内置ReturnType的实现原理)。
type ReturnType<T> = T extends (…args: any[]) => infer R ? R : never;
// 使用。
type F = () => string;
type R = ReturnType<F>; // string。
infer R捕获了函数返回值的类型string。
提取函数的参数类型(内置Parameters的实现)。
type Parameters<T> = T extends (…args: infer P) => any ? P : never;
// 使用。
type F = (a: number, b: string) => void;
type P = Parameters<F>; // [number, string]。
infer P捕获了参数列表的元组类型。
提取Promise的resolve类型(内置Awaited的简化版)。
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
// 使用。
type P = Promise<string>;
type U = UnwrapPromise<P>; // string。
type V = UnwrapPromise<number>; // number(不满足条件,返回原类型)。
infer U从Promise<string>中提取出string。
提取数组元素类型。
type ElementType<T> = T extends (infer E)[] ? E : T;
// 使用。
type Arr = string[];
type E = ElementType<Arr>; // type E = string。
type F = ElementType<readonly string[]>; // type F = readonly string[]。
注意:更健壮的写法应处理readonly数组:
type ElementType<T> = T extends readonly (infer E)[] ? E : T;
// 使用。
type Arr = string[];
type E = ElementType<Arr>; // type E = string。
type F = ElementType<readonly string[]>; // type F = string。
/*
为什么此处泛型限制为只读数组后,传递普通数组时,依旧可判定为真?
反而上述泛型限制为普通数组后,传递只读数组时,却判定为假?
该问题触及了TypeScript类型系统中一个重要的兼容性设计原则。我们来一步步解释为什么:
即使ElementType<T>中使用了"readonly (infer E)[]",普通的(非readonly)数组也能被匹配并正确推断出元素类型。
核心原因:TypeScript中"T[]"是"readonly T[]"的子类型。
也就是说:
可变数组(mutable array)可以赋值给只读数组(readonly array)变量,因此在类型匹配时,"number[]"被视为兼容"readonly number[]"。
这是TypeScript为了保持类型系统的实用性与安全性平衡而做的设计。
详细解释
1. 子类型关系(Subtyping)。
在TypeScript中,以下赋值是合法的:
const mutable: number[] = [1, 2, 3];
const readonlyMutable: readonly number[] = mutable; // 允许!
因为"readonly number[]"只承诺“我不会修改这个数组”,而mutable数组完全满足这个承诺
(即使它自己有能力修改【即mutable可修改数组】,但作为readonlyMutable使用时不会这么做【即readonlyMutable是只读的,但mutable修改后,也会同步给readonlyMutable,因为它们指向同一个数组】)。
这符合Liskov替换原则:子类型(可变数组)可以安全地用于需要父类型(只读数组)的地方。
因此,在类型检查中:
number[] extends readonly number[] → 成立(true)。
2. 应用到你的"ElementType<T>"类型。
type ElementType<T> = T extends readonly (infer E)[] ? E : T;
当你传入一个普通数组:
type R = ElementType<number[]>;
TypeScript会判断:
number[] extends readonly (infer E)[] 是否成立?
由于"number[]"是"readonly number[]"的子类型,条件成立,于是:
"infer E"成功推断出"E = number"。
最终结果:R = number。
所以即使你没写 readonly,也能正常工作!
验证实验:
// 普通数组
type A = ElementType<number[]>; // number。
type B = ElementType<string[]>; // string。
// 只读数组
type C = ElementType<readonly boolean[]>; // boolean。
// 非数组类型
type D = ElementType<string>; // string(走else分支)。
全部按预期工作!
那如果反过来呢?只写"T[]"能匹配"readonly T[]"吗?
答案:不能!
type ElementType<T> = T extends (infer E)[] ? E : T;
type X = ElementType<readonly number[]>; // readonly number[]。不匹配!
因为:
"readonly number[]"不能赋值给"number[]"(否则可能意外修改只读数据)。
所以:
readonly T[] extends T[] → 不成立。
条件失败,返回原类型"readonly number[]"。
记住:在TypeScript中,“只读”是一种更宽泛(supertype)的约束,可变类型可以安全地视为只读类型使用。
*/
提取类的构造函数参数(用于依赖注入等场景)。
type ConstructorArgs<T> = T extends new (…args: infer A) => any ? A : never;
class User {
constructor(name: string, age: number) {}
}
type Args = ConstructorArgs<typeof User>; // [string, number]。
infer的工作原理(关键点):
只能用在条件类型的extends右侧。
// 错误!不能在其他地方用infer。
type Bad<T> = infer U; // SyntaxError。
推断发生在类型匹配成功时。
只有当T extends XXX成立,infer才会尝试从T的结构中“反向解析”出U。
可以多次使用infer提取多个部分:
type FuncParts<T> = T extends (infer A) => infer B ? [A, B] : never;
type F = (x: number) => string;
type P = FuncParts<F>; // [number, string]。
支持嵌套和复杂结构。
type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T;
type X = DeepUnwrap<Promise<Promise<string>>>; // string。
实际应用场景:
| 获取函数返回值。 | ReturnType<T>。 |
| 获取函数参数。 | Parameters<T>。 |
| 获取构造函数参数。 | ConstructorParameters<T>。 |
| 解包Promise。 | Awaited<T>(TS 4.5+ 内置)。 |
| 提取数组/元组元素。 | 自定义ElementOf<T>。 |
| 实现高级类型操作。 | 如DeepPartial<T>,Flatten<T>。 |
常见误区:
infer是“声明变量”。
正确理解:infer是在类型匹配过程中动态推导并绑定类型,不是静态声明。
infer可以提取任意位置的类型。
实际:只能从满足结构匹配的类型中提取。
例如:
type Test<T> = T extends { data: infer D } ? D : never;
type R = Test<{ data: number }>; // number。
type R2 = Test<string>; // never(不匹配结构)。
infer总是返回联合类型。
实际:取决于上下文。
例如对函数重载的处理较复杂,但一般情况下按结构精确提取。
总结:
| 作用 | 在条件类型中“提取”并捕获子类型。 |
| 位置 | 仅限T extends … ? … : …的…(即extends右侧)。 |
| 本质 | 类型级别的模式匹配与解构。 |
| 用途 | 实现高级工具类型(如ReturnType、Parameters等)。 |
| 限制 | 依赖结构匹配,无法提取不满足条件的类型。 |
2.9 分布式条件类型
type ToArray1<Type> = Type extends any ? Type[] : never;
type ToArray2<Type> = Type extends { message: unknown } ? Type[] : never;
interface A {
message: string;
}
interface B {
message: number;
}
type StrArrOrNumArr = ToArray1<string | number>;
// type StrArrOrNumArr = string[] | number[]。
type StrObjOrNumObj = ToArray2<A | B>;
// type StrObjOrNumObj = A[] | B[]。
问题:
为何"StrArrOrNumArr"数据类型是string[] | number[]而不是(string | number)[]?
"StrObjOrNumObj"也一样,为什么是A[] | B[]而不是(A | B)[]?
注意:
string[] | number[]与(string | number)[]是两个完全不同的类型。
| string[] | number[] | 要么是纯字符串数组,要么是纯数字数组。 | 对:["a", "b"]对:[1, 2]错:["a", 1] |
| (string | number)[] | 数组元素可以是字符串或数字的混合。 | 对:["a", "b"]对:[1, 2]对:["a", 1, "b", 2] |
解答:
因为ToArray1<T>和ToArray2<T>是一个分布式条件类型(distributive conditional type),当T是联合类型(如string | number)时,TypeScript会自动将条件类型分别应用到联合类型的每个成员上,然后再把结果union起来。
ToArray1<string | number>
// 等价于
ToArray1<string> | ToArray1<number>
// 即
string[] | number[]
而不是(string | number)[]。
详细解释:
什么是“分布式条件类型”?
在TypeScript中,如果一个条件类型满足以下两个条件,它就是“分布式的”:
-
形如T extends U ? X : Y(这不是条件)。
-
T是一个类型参数(generic type parameter)。
-
T被直接用在extends左侧(没有被包裹在其他结构中)。
type ToArray<Type> = Type extends any ? Type[] : never;
// Type是泛型参数,直接出现在extends左侧 → 分布式!
分布式行为:自动拆解联合类型。
当传入联合类型A | B | C时,分布式条件类型会:
F<A | B> → F<A> | F<B>
为什么不是(string | number)[]?
因为(string | number)[]表示“一个数组,元素可以是string或number”,而上述ToArray1和ToArray2的逻辑是:“把每个类型T变成T[]”。
对联合类型string | number,它理解为:“要么是string类型 → 变成string[],要么是number类型 → 变成number[]”。
所以结果是两个数组类型的联合,而不是一个混合元素数组类型。
验证实验:
type ToArray<Type> = Type extends any ? Type[] : never;
type Result = ToArray<string | number>;
// Hover查看类型:string[] | number[]。
// 手动展开:
type Manual = ToArray<string> | ToArray<number>; // string[] | number[]。
// Result和Manual完全等价!
如何得到 (string | number)[]?
如果你不希望条件类型分布(即想把联合类型当作一个整体处理),你需要打破分布式条件类型的规则。
-
方法:用方括号[]包裹类型参数。
type ToArrayNonDistributive<Type> = [Type] extends [any] ? Type[] : never;
type Result2 = ToArrayNonDistributive<string | number>;
// 结果:(string | number)[]。 -
原理:
-
[Type] extends [any]中,Type被包裹在元组中,不再是“裸类型参数”。
-
因此不触发分布式行为。
(不满足条件:T被直接用在extends左侧(没有被包裹在其他结构中))。
-
整个string | number被当作一个整体传入,结果就是(string | number)[]。
-
实际意义:
-
分布式条件类型是很多内置工具类型的基础,例如:
type Exclude<T, U> = T extends U ? never : T;
type E = Exclude<"a" | "b" | "c", "a">; // "b" | "c"。正是靠分布式行为,才能逐个过滤联合类型成员。
-
如果你不小心写出了分布式条件类型,但本意是整体处理,就会出现“意外拆解”的问题。
最佳实践建议:
| 对联合类型每个成员分别处理(如Exclude, Extract)。 | 用裸类型参数:T extends …。 |
| 把联合类型当作一个整体。 | 包裹起来:[T] extends […]或{ x: T } extends { x: … }。 |

