红茶的个人站点

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

TypeScript 学习笔记 5:类型操作

2026年2月25日 66点热度 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)[]

映射类型

前面已经说过,可以使用索引签名约束对象类型的属性:

type Booleans = {
    [attr:string]: boolean;
}
​
const CheckList: Booleans = {
    check1: true,
    check2: false,
    check3: true,
}

在索引签名中,除了直接定义索引名称的类型([attr:string]),还可以将索引名称映射到某个类型的索引类型联合:

type Booleans2<T> = {
    [attr in keyof T]: boolean;
}
​
type Fish = {
    name: string;
    swim: () => void;
}
​
type CheckList2 = Booleans2<Fish>; // 类型推断为 { name: boolean; swim: boolean; }

映射修饰符

在进行类型映射的时候,可以使用属性修饰符readonly和?,并且可以使用+或-添加或删除对应的修饰符:

type ReadonlyPerson = {
    readonly name: string;
    readonly age: number;
}
​
type Writable<T> = {
    -readonly [key in keyof T]: T[key];
}
​
type WritablePerson = Writable<ReadonlyPerson>;
const tom: WritablePerson = { name: 'Tom', age: 20 };
tom.age = 21;

对于修饰符?,是类似的:

type OptionalPerson = {
    name: string;
    age?: number;
    career?: string;
}
​
type Full<T> = {
    [key in keyof T]-?: T[key];
}
​
type FullPerson = Full<OptionalPerson>; // 类型推断为 { name: string; age: number; career: string; }

as

从 TS 4.1 开始,可以使用关键字as在类型映射时重定义键名:

type Getter<T> = {
    [key in keyof T as `get${Capitalize<string & key>}`]: () => T[key];
}
​
type Car2 = {
    brand: string;
    model: string;
}
​
type CarGetter = Getter<Car2>; // 类型推断为 { getBrand(): string; getModel(): string; }

Capitalize是一个 TS 的预定义类型,其用途是将字符串类型的首字母大写。比如Capitalize<'hello'>的结果是'Hello'类型。

在上面这个示例中,利用重定义键名,定义了一个将指定类型转换为 Getter 类型的Getter。

另外一种应用是排除特定的属性:

type NoId<T> = {
    [key in keyof T as key extends 'id' ? never : key]: T[key];
}
​
type Person3 = {
    id: number;
    name: string;
    age: number;
}
​
type PersonWithoutId = NoId<Person3>; // 类型推断为 { name: string; age: number; }

被映射的类型还可以是任意类型的联合,比如:

type HandleEvents<Events extends {kind:string}> = {
    [Event in Events as Event['kind']]: (event: Event) => void;
}
​
type SquareEvent = {
    kind: 'square';
    size: number;
}
​
type RectangleEvent = {
    kind: 'rectangle';
    width: number;
    height: number;
}
​
type EventHandlers = HandleEvents<SquareEvent | RectangleEvent>;
// 类型推断为 {
//     square: (event: SquareEvent) => void;
//     rectangle: (event: RectangleEvent) => void;
// }

模版字面量类型

可以像在普通 JS 代码中那样在 TS 的类型上下文中使用模版字符串构造字符串类型:

type Hello = 'Hello';
type HelloWorld = `${Hello} World`; // 类型推断为 "Hello World"

与普通 JS 代码不同的是,可以在模版字符串中使用类型联合:

type Hello2 = 'Hello';
type Bye2 = 'Bye';
type Jack = 'Jack';
type Tom = 'Tom';
type Message = `${Hello2 | Bye2} ${Jack | Tom}`;
// 类型推断为 "Hello Jack" | "Hello Tom" | "Bye Jack" | "Bye Tom"

最终的字符串类型是原始字符串类型组合的笛卡尔积。

类型中的字符串联合

看一个示例:

type WatchableObject ={
    on(eventName: string, callback: () => void): void;
}
function makeWatchedObject<T extends object>(obj: T): T & WatchableObject{
    const newObj = {... obj};
    (newObj as WatchableObject).on = (eventName: string, callback: () => void) => { 
        console.log(`Event: ${eventName}`);
    };
    return newObj as T & WatchableObject;
}
const person = { name: 'Tom', age: 10 };
const watchedPerson = makeWatchedObject(person);
watchedPerson.on('ageChanged', () => {
    console.log(`I'm now ${watchedPerson.age} years old`);
});

WatchableObject表示一种可以监视属性改变的对象,可以通过其on方法添加属性改变的监视函数:

watchedPerson.on('ageChanged', () => {
    console.log(`I'm now ${watchedPerson.age} years old`);
});

函数makeWatchedObject用于具体的将普通对象转化为可以监视的对象,这只是一个简单示例,并没有真正实现属性监视,只是简单的为目标对象创建了on方法。

on方法需要指定一个事件名称和钩子函数,一般而言,事件名称是按照一定规则定义好的,比如示例中,年龄属性(age)改变的事件名称就应该是ageChanged。

如果要将这种规则约束体现在类型定义中,需要修改代码:

type WatchableObject<T> = {
    on(eventName: `${string & keyof T}Changed`, callback: () => void): void;
}
function makeWatchedObject<T extends object>(obj: T): T & WatchableObject<T>{
    const newObj = {... obj};
    (newObj as WatchableObject<T>).on = (eventName: string, callback: () => void) => { 
        console.log(`Event: ${eventName}`);
    };
    return newObj as T & WatchableObject<T>;
}

现在,TS 可以严格检查事件名称,不符合规则的名称就会报错:

watchedPerson.on('ageChanged', () => {
    console.log(`I'm now ${watchedPerson.age} years old`);
});
watchedPerson.on('nameChanged', ()=>{
    console.log(`I'm now called ${watchedPerson.name}`);
});
watchedPerson.on('schoolChanged', ()=>{  // 报错
});
watchedPerson.on('nameUpdated', ()=>{  // 报错
});

使用模版字面量进行推断

假设这里的on接收的钩子方法需要接收一个参数,这个参数表示修改后的新属性值,我们可以:

type WatchableObject<T> = {
    on(eventName: `${string & keyof T}Changed`, callback: (newVal: any) => void): void;
}

但这样有点太宽泛了,可以进一步推断并约束类型,让其与被改变的属性类型一致:

type WatchableObject<T> = {
    on<Key extends keyof T & string>(eventName: `${Key}Changed`, callback: (newVal: T[Key]) => void): void;
}

绑定事件时钩子函数的参数类型会被准确推断:

watchedPerson.on('ageChanged', (newVal) => { // newVal 推断为 number
    console.log(`I'm now ${newVal} years old`);
});
watchedPerson.on('nameChanged', (newVal) => { // newVal 推断为 string
    console.log(`I'm now called ${newVal}`);
});

内置字符串操作类型

TS 内置一些类型,用于对字符串类型进行一些常见操作,这些预定义类型内置在 TS 的编译器中。

Uppercase用于将字符串转换为全大写:

type HelloWorld2 = 'Hello World';
type UppercaseHelloWorld = Uppercase<HelloWorld2>; //类型推断为 "HELLO WORLD"

Lowercase用于将字符串转换为全小写:

type LowercaseHelloWorld = Lowercase<HelloWorld2>; // 类型推断为 "hello world"

Capitalize用于将字符串转换为首字母大写:

type HelloWorld3 = 'hello world';
type CapitalizeHelloWorld = Capitalize<HelloWorld3>; // 类型推断为 "Hello world"

Uncapitalize用于将字符串转换为首字母小写:

type HelloWorld4 = 'Hello world';
type UncapitalizeHelloWorld = Uncapitalize<HelloWorld4>; // 类型推断为 "hello world"

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号