红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 前端学习笔记
  3. 正文

TypeScript 学习笔记 5:类型操作

2026年2月25日 4点热度 0人点赞 0条评论

泛型

泛型函数

定义一个泛型函数:

function returnParameter<T>(param: T): T {
  return param;
}

调用函数:

returnParameter<string>('hello');

调用泛型函数时并不是一定要指定泛型类型,也可以省略泛型类型,让 TS 编译器根据实际参数的类型推断:

returnParameter('world');

但有时候遇到复杂的调用,TS 无法正确推断出泛型类型,就需要手动指定。

泛型函数类型

在 TS 中,可以用函数类型来约束函数对象,同样的,如果函数对象指向一个泛型函数,可以用泛型函数类型进行约束和定义:

let returnParamFunc: <T>(param: T) => T = returnParameter;

泛型函数类型和普通的函数类型类似,只不过需要用<...>指定泛型参数。

除了泛型函数类型,也可以用可调用对象的方式描述泛型函数类型:

let returnParamFunc2: { <T>(param: T): T } = returnParameter;

或者用interface定义一个可调用对象,再进行函数类型声明:

interface ReturnParameterFunc {
    <T>(param: T): T;
}
let returnParamFunc3: ReturnParameterFunc = returnParameter;

泛型类

泛型类的定义方式与interface类似:

class GenericNumber<T> {
    zeroValue?: T;
    add?: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) { return x + y; };
console.log(myGenericNumber.add(1, 2));

泛型约束

有时候,你希望泛型函数中的泛型类型具备某种特性:

function loggingIdentity2<T>(arg: T): T {
    console.log(arg.length); // 报错,类型“T”上不存在属性“length”。
    return arg;
}

这里希望泛型类型的参数arg必须拥有length属性。

可以自定义一个拥有length属性的类型,并使用这个类型约束泛型参数的范围:

interface Lengthwise {
    length: number;
}
​
function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);
    return arg;
}
​
console.log(loggingIdentity<string>('string'));
console.log(loggingIdentity<number>(1)); // 报错,类型“number”不满足约束“Lengthwise”
console.log(loggingIdentity<boolean>(true)); // 报错,类型“boolean”不满足约束“Lengthwise”
console.log(loggingIdentity<{ length: number }>({ length: 10 }));

类型参数约束

可以用另一个泛型参数约束当前的泛型参数,比如:

function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
    return obj[key];
}
const obj = { 'a': 1, 'b': 2, 'c': 3 }
console.log(getProperty(obj, 'a'));
console.log(getProperty(obj, 'd')); // 报错,类型“"d"”的参数不能赋给类型“"a" | "b" | "c"”的参数

这里的泛型参数Key指定为泛型参数Type的key。

在泛型中使用类类型

看一个在 JS 中使用工厂模式创建对象的示例:

class Person {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
}
​
function createPerson(name: string): Person {
    return new Person(name);
}
​
const p = createPerson('张三');

这里createPerson是一个工厂函数,用于创建Person对象。

当然一般会有更复杂的抽象层次,比如:

class Person {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
}
​
class Student extends Person {
    id: number;
    constructor(name: string, id: number) {
        super(name);
        this.id = id;
    }
}
​
class Teacher extends Person {
    courseName: string;
    constructor(name: string, courseName: string) {
        super(name);
        this.courseName = courseName;
    }
}

如果依然需要用一个工厂函数去创建Person的不同派生类对象,就需要指定派生类的类型和构造参数:

const p = createPerson(Person, '张三');
const s = createPerson(Student, '张三', 1);
const t = createPerson(Teacher, '张三', 'Javascript');

这样做是可行的,因为 JS 中类类型本身也是一个对象,可以作为参数传递。

此时的工厂函数使用泛型表示接收的类类型,并用基类进行约束:

function createPerson<T extends Person>(constractor: { new(...args: any[]): T }, ...args: any[]): T {
    return new constractor(...args);
}

这里表示类类型的参数constractor,使用{ new(...args: any[]): T }进行类型定义,它说明了对应的类类型的构造函数是怎样的。

keyof

运算符keyof作用于一个对象类型,返回对象类型所有属性名称的联合:

type Perosn2 = { name: string; age: number; job: string };
type PersonKeys = keyof Perosn2;
const key: PersonKeys = 'name';
const key2: PersonKeys = 'age';
const key3: PersonKeys = 'job';
const key4: PersonKeys = 'sex'; // 报错

这里keyof Perosn2返回的类型实际上是name|age|job。

如果对象类型使用了索引签名,则keyof获取到的是索引名称中定义的类型:

type numberAttrObj = {[key: number]: any};
type keys = keyof numberAttrObj; // 类型推断为 number
const key5: keys = 1;
const key6: keys = 2;

比较特殊的是:

type Mapish = { [k: string]: boolean };
type M = keyof Mapish; // 类型推断为 string | number
const key7: M = 1;

这里M的类型是string|number而不是string。这是因为虽然索引签名中定义了属性名称都是string类型,但在 JS 中,你依然可以使用数字进行索引,比如a[0],此时数字索引会被强行转化为字符串索引(a['0'])。

typeof

JS 在表达式上下文中,提供一个操作符typeof,可以获取变量的类型:

function printParam(param: any){
    if(typeof param === 'string'){
        console.log(param.toUpperCase());
    }
    else if(typeof param === 'number'){
        console.log(param.toFixed(2));
    }
    else{
        console.log(param);
    }
}
​
printParam('hello');
printParam(123.456);
printParam({});

TS 中,可以在类型上下文中使用typeof获取类型,比如:

let s1 = "hello";
let s2: typeof s1 = "world";

这里s2的类型使用的是typeof s1,表示和s1的类型相同。

这样做或许显得有点画蛇添足,但在复杂类型中,typeof是有用途的,比如:

type Predicate = (x: unknown) => boolean;
type K = ReturnType<Predicate>; // 类型推断为 boolean

这里的ReturnType是一个预定义类型,可以利用它获取到特定函数类型的返回值类型。但需要注意的是,这里只能通过泛型参数指定函数类型,而不是函数名称:

function isOk(x: unknown){
    return x === 'ok';
}
type K2 = ReturnType<isOk>; // 报错

可以使用typeof:

type K3 = ReturnType<typeof isOk>;

索引访问类型

可以用索引的方式使用对象类型的属性类型:

type Person2 = { name: string; age: number };
type Name = Person2['name']; // 类型推断为 string
type Age = Person2['age']; // 类型推断为 number

这里Person2['name']表示对象类型Person2的属性name的类型。

在索引中可以使用任意类型,包括类型联合:

type NameOrAge = Person2['name' | 'age']; // 类型推断为 string | number
type NameOrAge2 = Person2[keyof Person2];

特殊的,可以使用[number]的方式获取数组元素的类型:

const myArray = [{name:'xiaoming',age:18},{name:'zhangsan',age:16}]
type MyArrayElement = typeof myArray[number]; // 类型推断为 {name: string; age: number;}
type ElementName = MyArrayElement['name']; // 类型推断为 string
type ElementAge = MyArrayElement['age']; // 类型推断为 number

条件类型

可以在类型上下文中使用条件表达式:

interface Animal{
    eat(food:any):void;
}
​
interface Dog extends Animal{
    bark():void;
}
​
interface Car{
    drive():void;    
}
​
type DogIsAnimal = Dog extends Animal ? true : false; // 类型推断为 true
type CarIsAnimal = Car extends Animal ? true : false; // 类型推断为 false

这里的类型条件表达式Dog extends Animal ? true : false,如果条件成立(Dog是Animal的扩展类型),表达式的结果就是:前的类型(true),否则就是:后边的类型(false)。

看一个例子:

interface IdLabel {
    id: number;
}
​
interface NameLabel {
    name: string;
}
​
function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
    if (typeof nameOrId === 'string') {
        return { name: nameOrId };
    }
    return { id: nameOrId };
}
​
const a = createLabel('xiaoming');

这里用重载的方式实现了createLabel函数,它可以根据不同类型的参数返回IdLabel或NameLabel。

可以使用泛型和条件类型表达式重写这个示例,这样就不需要使用函数重载:

type IdOrNameLabel<T extends string | number> = T extends string ? NameLabel : IdLabel;
function createLabel2<T extends string | number>(nameOrId: T): IdOrNameLabel<T> {
    if (typeof nameOrId === 'string') {
        return { name: nameOrId } as IdOrNameLabel<T>;
    }
    return { id: nameOrId } as IdOrNameLabel<T>;
}
​
const b = createLabel2('xiaoming');
const c = createLabel2(1);
console.log(b, c);

这里的as IdOrNameLabel<T>是因为 TS 无法正确推断出类型而添加。

条件类型约束

如果要设计一个获取指定类型指定属性的类型别名:

type MessageOf<T> = T['message']; // 报错,类型“T”上不存在属性“message”。

这样会报错,因为不能确保泛型参数T一定具备指定属性,因此需要对泛型参数进一步约束:

type MessageOf2<T extends { message: unknown }> = T['message'];
​
interface Email {
    message: string;
}
​
type EmailMessageContents = MessageOf2<Email>; // 类型推断为 string

这样是没有问题的,但MessageOf2只能用于满足约束T extends { message: unknown }的类型,如果我们要用于任意类型,就需要使用类型条件表达式:

type MessageOf3<T> = T extends { message: unknown } ? T['message'] : never;
type EmailMessageContents2 = MessageOf3<Email>; // 类型推断为 string
type DogMessageContents = MessageOf3<Dog>; // 类型推断为 never

infer

在上面的示例中,使用索引的方式获取对象类型中的指定属性类型,除了这种方式,还可以使用infer关键字:

type MessageOf4<T> = T extends {message: infer Message} ?  Message : never;
type EmailMessageContents3 = MessageOf4<Email>; // 类型推断为 string
type DogMessageContents2 = MessageOf4<Dog>; // 类型推断为 never

在 TS 中,infer关键字可以用于类型占位符,示例中的infer Message表示Message是message属性的类型。

再看一个示例,用infer获取数组元素类型:

type ArrayElement<T> = T extends Array<infer U> ? U : never;
const arr = [1, 2, 3];
type ArrayElementType = ArrayElement<typeof arr>; // 类型推断为 number

下面的示例是获取函数返回值类型:

type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function f1(): string {
    return 'hello';
}
​
function f2(arg: number): number {
    return arg;
}
​
function f3(){}
​
type F1ReturnType = GetReturnType<typeof f1>; // 类型推断为 string
type F2ReturnType = GetReturnType<typeof f2>; // 类型推断为 number
type F3ReturnType = GetReturnType<typeof f3>; // 类型推断为 void
type F4ReturnType = GetReturnType<string>; // 类型推断为 never

有一个细节,如果用GetReturnType获取重载函数的返回值,TS 最终的推断类型会是重载函数签名中最后一个签名的返回值类型:

function f4(id:number):number;
function f4(name:string):string;
function f4(arg:number|string):number|string;
function f4(arg:number|string):number|string{ 
    return arg;
}
​
type F4ReturnType2 = GetReturnType<typeof f4>; // 类型推断为 number | string

分发条件类型

利用类型条件表达式定义一个类型,将给定类型转换为对应的数组类型:

type ToArray<T> = T extends any ? T[] : never;

如果给定的类型是联合类型,就会出现类型分发:

type ToArrayType = ToArray<string|number>; // 类型推断为 string[] | number[]

这里最终推断的类型是string[] | number[],而不是(string|number)[]。

TS 默认在这种情况下会进行类型分发,如果你不希望这种行为产生,可以:

type ToArray2<T> = [T] extends [any] ? T[] : never;
type ToArrayType2 = ToArray2<string|number>; // 类型推断为 (string|number)[]

The End.

参考资料

  • TypeScript 官方手册

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: ts
最后更新:2026年2月25日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号