一、keyof类型操作符
TypeScript中的keyof类型操作符可以获取某个类型的所有属性名组成的联合类型。这个操作符的作用是帮助开发者在静态类型检查中更准确地操作属性名。
举例来说,如果我们有如下一个接口:
interface Person {
name: string;
age: number;
gender: 'male' | 'female';
}
我们可以使用keyof来获取这个接口的属性名联合类型:
type PersonKeys = keyof Person;
// 等价于:
// type PersonKeys = 'name' | 'age' | 'gender'
有了属性名联合类型,我们可以在编写代码时更准确地操作属性名。以下是一些使用keyof的实际应用:
1. 动态获取对象的属性值
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
const person: Person = { name: 'Lucy', age: 18, gender: 'female' };
const name = getProperty(person, 'name'); // 类型为string
const age = getProperty(person, 'age'); // 类型为number
const gender = getProperty(person, 'gender'); // 类型为'male' | 'female'
在这个例子中,getProperty函数的第一个参数是一个泛型类型的对象,第二个参数是对象的属性名。由于我们使用了keyof,所以在编写代码时我们可以确定属性名的类型,并且编译器也可以在编译时进行类型检查,保证我们不会误操作属性名或者试图访问不存在的属性。
2. 限制对象的属性种类
function createUser<Keys extends keyof Person>(name: string, value: Person[Keys]): Person {
const user: Person = { name, age: 0, gender: 'male' };
user[key] = value; // 编译器知道key是Person的属性名之一,不会有任何错误
return user;
}
const newUser = createUser('Tom', 'male'); // 类型为Person
const errorUser = createUser('Jack', 'unknown'); // 编译错误
在这个例子中,createUser函数通过泛型限制了属性名的类型,而属性名的类型只能从Person的属性名中取值。在函数内部,我们可以安全地使用key来访问person对象的属性,因为key的类型是Person的属性名之一。如果我们试图传入一个不合法的属性名,编译器会及时提示错误。
3. 避免硬编码属性名
class User {
constructor(private data: Person) {}
get<K extends keyof Person>(key: K): Person[K] {
return this.data[key];
}
}
const user = new User({ name: 'Lucy', age: 18, gender: 'female' });
const name = user.get('name'); // 类型为string
const age = user.get('age'); // 类型为number
const gender = user.get('gender'); // 类型为'male' | 'female'
在这个例子中,User类接受一个Person对象作为构造函数的参数,同时提供了一个get方法来获取属性值。我们使用了泛型和keyof,这样在代码中就不需要硬编码属性名,避免了潜在的错误。泛型的约束可以帮助我们在编译时确保只能传入合法的属性名。
二、typeof类型操作符
TypeScript中的typeof
类型操作符可以用来获取一个值的类型信息,它返回一个代表该值类型的字符串。typeof操作符不会运行代码,只会在编译时进行类型检查。
使用typeof类型操作符的场景包括:
1. 类型检查:可以用来检查变量的类型。例如,可以使用typeof操作符来检查变量是不是一个字符串。
let str = 'hello world';
if (typeof str === 'string') {
console.log('str is a string');
}
2. 类型推断:可以使用typeof来推断函数返回值的类型。
function double(num: number): number {
return num * 2;
}
let num = 10;
let numDouble = double(num); // numDouble的类型被推断为number
if (typeof numDouble === 'number') {
console.log('numDouble is a number');
}
3. 编写工具函数:可以使用typeof来编写工具函数,比如判断一个值是不是一个数组。
function isArray(value: any): value is Array<any> {
return typeof value === 'object' && value !== null && Array.isArray(value);
}
let arr = [1, 2, 3];
if (isArray(arr)) {
console.log('arr is an array');
}
4. 简化重复代码:可以使用typeof来简化重复代码,比如初始化一个对象中的属性值。
interface Person {
name: string;
age: number;
}
function createPerson(name: string, age: number): Person {
return {
name,
age,
id: typeof name === 'string' ? name.slice(0, 3).toUpperCase() + age.toString() : '',
};
}
let person = createPerson('John', 30);
console.log(person.id); // JOH30
总之,typeof类型操作符在TypeScript中有很多用途,可以帮助开发者更好地编写类型安全的代码。
三、索引访问类型
索引访问类型在TypeScript中是一种用于获取类型中属性或元素的方式,它通过字符串或数字索引来访问具有下标的类型。它通常用于动态访问对象属性、数组元素和元组元素等。下面我们从多个角度介绍索引访问类型并举例说明。
1. 动态访问对象属性
假设我们有一个Person对象类型,它有两个属性name和age。我们可以通过索引访问类型来动态获取对象的属性值。
type Person = { name: string; age: number };
type Name = Person['name']; // string
type Age = Person['age']; // number
const person: Person = { name: 'John', age: 18 };
function getProperty(obj: Person, key: keyof Person) {
return obj[key];
}
const name: string = getProperty(person, 'name'); // 'John'
const age: number = getProperty(person, 'age'); // 18
在上述代码中,我们定义了一个Person类型,并使用Person[‘name’]和Person[‘age’]来获取分别获取name和age的类型。我们使用keyof Person类型指定getProperty方法中的key参数只能传递Person对象的属性名。最后我们通过getProperty方法动态获取person对象的属性值。
2. 动态访问数组元素
索引访问类型也可以用于动态访问数组元素。我们可以通过索引类型来获取数组元素的类型,也可以通过keyof Array类型来获取数组的索引类型。
const fruits = ['apple', 'banana', 'orange'];
type Fruit = typeof fruits[number]; // 'apple' | 'banana' | 'orange'
type Index = keyof typeof fruits; // number | "length" | "toString" | "toLocaleString" | "push" | "pop" | "concat" | "join" | "reverse" | "shift" | "slice" | "sort" | "splice" | "unshift" | "indexOf" | "lastIndexOf" | "every" | "some" | "forEach" | "map" | "filter" | "reduce" | "reduceRight" | "entries" | "forEach" | "keys" | "values"
上述代码中,我们定义了一个数组fruits,并使用typeof fruits[number]和keyof typeof fruits分别获取了它的元素类型和索引类型。
3. 动态访问元组元素
元组是一种特殊的数组类型,其元素类型可以不同。我们可以通过索引访问类型来动态访问元组元素。
type Tuple = [string, number];
type First = Tuple[0]; // string
type Second = Tuple[1]; // number
const tuple: Tuple = ['hello', 123];
function getTupleElement<T extends ReadonlyArray<any>, U extends keyof T>(
tuple: T,
index: U,
): T[U] {
return tuple[index];
}
const first: string = getTupleElement(tuple, 0); // 'hello'
const second: number = getTupleElement(tuple, 1); // 123
在上述代码中,我们定义了一个元组类型Tuple,通过Tuple[0]和Tuple[1]分别获取了它的第一个和第二个元素类型。同时,我们定义了一个getTupleElement方法,使用泛型T和U分别表示元组类型和索引类型,并通过T[U]获取元组指定索引处的元素。
总之,索引访问类型可以用于动态访问对象属性、数组元素和元组元素。它可以方便地处理动态类型,增强了TypeScript的灵活性和适用性。
四、条件类型
TypeScript中的条件类型可以根据某个类型的特定属性或条件,选择不同的类型。条件类型是TypeScript高级类型中的一种,可以用于定义泛型的约束条件,从而增强代码的类型安全性和灵活性。
下面从不同角度举例分析说明TypeScript中的条件类型:
1. 根据属性判断是否可选
条件类型可以根据某个属性是否存在或者是否可选来确定不同的类型。例如,以下代码中,当T中的K属性为可选属性时,返回Partial类型;当K为必选属性时,返回T本身:
type MyType<T, K extends keyof T> = K extends keyof T ? Partial<T> : T;
2. 根据属性值判断是否满足条件
条件类型可以根据某个属性的值是否满足条件来确定不同的类型。例如,以下代码中,当T中的K属性的类型为U时,返回T本身;否则返回never类型:
type MyType<T, K extends keyof T, U> = T[K] extends U ? T : never;
3. 根据类型之间的关系判断是否满足条件
条件类型可以根据不同类型之间的关系来确定不同的类型。例如,以下代码中,当T为U的子类型时,返回T本身;否则返回never类型:
type MyType<T, U> = T extends U ? T : never;
4. 根据函数参数类型判断返回值类型
条件类型可以根据函数参数的类型来确定函数返回值类型。例如,以下代码中,当T为函数类型时,返回函数返回值的类型;否则返回never类型:
type MyType<T> = T extends (...args: any[]) => infer R ? R : never;
5. 根据对象类型判断是否有特定属性
条件类型可以根据对象类型中是否含有特定属性来确定不同的类型。例如,以下代码中,当T中含有名为K的属性时,返回T本身;否则返回never类型:
type MyType<T, K> = keyof T extends K ? T : never;
通过以上几个例子,可以看出条件类型在TypeScript中的灵活性和强大的约束能力。条件类型可以根据不同的情况进行不同的判断,从而增强代码的可读性和可维护性。
五、类型推理infer
在TypeScript中,类型推理是一种自动推断变量类型的机制,它可以根据变量的使用上下文以及其值的类型来推断变量的类型。而infer
关键字是TypeScript的一种高级类型操作符,它可以用来从已知类型中推断出未知类型,让类型推理更加灵活。
infer关键字通常在条件类型中使用,其中条件类型可以根据条件来决定返回的类型。infer用于捕获条件类型中的未知类型,然后可以用该类型来进行操作。下面分别从多角度举例说明infer如何使用。
1. 从函数参数中推断类型
在下面的这个例子中,我们可以看到如何使用infer关键字从函数参数中推断出其类型:
type ParameterType<T extends (...args: any) => any> = T extends ((arg: infer P) => any) ? P : never;
function foo(param: string) {}
type ParamType = ParameterType<typeof foo>; // string
在这个例子中,我们定义了一个ParameterType类型,它接受一个函数类型作为参数。然后,我们使用infer关键字来推断函数类型的参数类型,并将此类型指定为类型别名ParamType的值。
2. 从数组或元组中推断类型
在下面的这个例子中,我们可以看到如何使用infer关键字从数组或元组中推断出其类型:
type ArrayType<T> = T extends Array<infer U> ? U : never;
type TupleType<T> = T extends [infer U, ...infer V] ? [U, ...V] : never;
type A = ArrayType<number[]>; // number
type B = TupleType<[string, number, boolean]>; // [string, number, boolean]
在这个例子中,我们定义了两个类型别名ArrayType和TupleType。ArrayType接受一个数组类型作为参数,使用infer关键字推断出数组元素的类型,并将其作为其返回类型。TupleType接受一个元组类型作为参数,使用infer关键字推断出元组中第一个元素的类型,并使用剩余类型推断出元组剩余的元素类型。然后,我们分别将这些类型应用于变量A和变量B,得到相应的类型结果。
3. 从Promise中推断类型
在下面的这个例子中,我们可以看到如何使用infer关键字从Promise中推断出其类型:
type PromiseType<T> = T extends Promise<infer U> ? U : never;
async function foo(): Promise<string> {
return 'hello';
}
type FooType = PromiseType<ReturnType<typeof foo>>; // string
在这个例子中,我们定义了一个PromiseType类型,它接受一个Promise类型作为参数,使用infer关键字推断出Promise的值类型,并将其作为其返回类型。然后,我们定义了一个异步函数foo,其返回类型为Promise。最后,我们将函数foo的ReturnType应用于PromiseType类型,并得到其返回值类型为string的结果。
总之,infer是TypeScript中一个非常重要的高级类型操作符,可以用于从已知类型中推断出未知类型,让类型推理更加灵活。通过上述多个角度的示例,希望可以更好地理解infer在TypeScript中的实际应用。
六、分布式条件类型
分布式条件类型是TypeScript的一项高级特性,它也是条件类型的一种。分布式条件类型可以根据一个类型参数 T,在联合类型中判断 T 是否为其他类型,并根据 T 是该类型或其子集来生成新类型。这个生成过程会在联合类型中遍历每一个元素,并生成对应的类型。与普通的条件类型不同的是,分布式条件类型会把联合类型的操作分发到每一个元素中。
下面我们通过举例分析,更全面地了解分布式条件类型。
1、 基本使用
首先,我们看一个最基本的例子:
type IfNumber<T> = T extends number ? 'yes' : 'no';
type A = IfNumber<1>; // 'yes'
type B = IfNumber<'a'>; // 'no'
type C = IfNumber<number | string>; // 'yes' | 'no'
这里,我们定义了一个条件类型IfNumber,当T是number类型的时候返回’yes’,否则返回’no’。当我们分别传入1、‘a’、number | string三种类型作为类型参数进行测试时,分别返回’yes’、‘no’、‘yes’ | ‘no’。
2、 分布式条件类型在泛型中的应用
分布式条件类型可以用在泛型中,作为泛型约束的一部分。比如,我们可以定义一个函数,用于获取对象的属性值。如果对象的属性值是数字,返回数字类型的对象,否则返回字符串类型的对象。
type ObjectValue<T> = T extends { [key: string]: infer U } ? U : never;
type ObjectWithType<T, U> = { [K in keyof T]: ObjectValue<T[K]> extends U ? T[K] : never };
type ExtractObject<O, U> = ObjectWithType<O, U>[keyof O];
function getPropByType<O, U>(obj: O, type: U): ExtractObject<O, U>[] {
const result = [];
for (const key in obj) {
const value = obj[key];
if (typeof value === typeof type) {
result.push(value);
}
}
return result;
}
const obj = {
a: 1,
b: '2',
c: 3,
};
const result = getPropByType(obj, '2');
这里,我们使用分布式条件类型ObjectWithType来定义一个新类型ObjectWithType<T, U>,它可以将T中每个属性值的类型做比较,只有当属性值的类型===U时才保留该属性。同时,我们使用ObjectValue来辅助获取对象属性值的类型。在getPropByType函数中,我们传入一个对象和一个类型参数U,函数会遍历对象的属性值,检查它们的类型是否等于U,并将符合条件的属性值保存在数组中后返回。
3、 分布式条件类型在类型映射中的应用
分布式条件类型还可以用于类型映射中。下面我们使用分布式条件类型,实现一个将对象中所有属性变成可选属性的函数。
type Optionalize<T> =
T extends any ? {
[K in keyof T]?: Optionalize<T[K]>;
} : never;
function optionalize<Key extends string, O extends { [K in Key]: any }>(obj: O): Optionalize<O> {
const result: Optionalize<O> = {};
for (const key in obj) {
const value = obj[key];
if (typeof value === 'object' && !Array.isArray(value)) {
result[key] = optionalize(value);
} else {
result[key as keyof O] = value;
}
}
return result;
}
const obj = {
a: {
b: 1,
c: {
d: 2,
},
},
e: '3',
};
const optionalObj = optionalize(obj);
这里,我们使用了分布式条件类型来定义Optionalize类型。它首先认为T可以是任何类型,然后对于T中的每个属性K,我们都将它变成一个可选属性。在optionalize函数中,我们分别遍历了obj的所有属性,并根据value的类型决定是递归处理还是复制value的值到result中,最终返回了一个可选属性合集Optionalize。
分布式条件类型是一项高级特性,它可以对联合类型的所有元素进行操作,生成多样化的新类型。我们在实际场景中可以通过它来解决一些复杂的问题,比如提取对象中某个类型的属性、将对象中的属性转换成可选属性。掌握这个知识点,可以让我们更好地应对类型转换的挑战。