泛型在开发组件或库时非常有用
在本文中,我将介绍如何使用TypeScript泛型来声明 VueBasicProps
函数来完成以下挑战。在挑战中,我还会介绍一些非常有用的TypeScript知识。掌握了以后,应该会对你的工作有所帮助。
挑战
在“TypeScript泛型的高级用法第1部分”一文中,我们声明了 SimpleVue
函数。接下来,我将扩展这个函数的功能,向原来的 options
形参对象添加一个新的 props
属性。这是Vue props
选项的简化版本。这里有一些规则。
-
props
是一个对象,该对象的每个属性都会被注入到this
上下文中。注入的props
属性可以在所有上下文中访问,包括data
、computed
和methods
。 -
单个
prop
属性可以通过构造函数或具有type
属性的对象来定义。
例如:
props: {
foo: Boolean
}
// or
props: {
foo: { type: Boolean }
}
上述道具将被推断为 type Props = { foo: boolean }
。当传递多个构造函数时,应该将属性的类型推断为联合类型。
props: {
foo: { type: [Boolean, Number, String] }
}
// -->
type Props = { foo: boolean | number | string }
如果当前属性的类型为空对象类型( {}
),则该属性的类型将被推断为类型 any
。
VueBasicProps
函数的使用示例如下:
class ClassA {}
VueBasicProps({
props: {
propA: {},
propB: { type: String },
propC: { type: Boolean },
propD: { type: ClassA },
propE: { type: [String, Number] },
propF: RegExp,
},
data(this) {
type PropsType = Debug<typeof this>
type cases = [
Expect<IsAny<PropsType['propA']>>,
Expect<Equal<PropsType['propB'], string>>,
Expect<Equal<PropsType['propC'], boolean>>,
Expect<Equal<PropsType['propD'], ClassA>>,
Expect<Equal<PropsType['propE'], string | number>>,
Expect<Equal<PropsType['propF'], RegExp>>,
]
// @ts-expect-error
this.firstname
// @ts-expect-error
this.getRandom()
// @ts-expect-error
this.data()
return {
firstname: 'Type',
lastname: 'Challenges',
amount: 10,
}
},
computed: {
fullname() {
return `${this.firstname} ${this.lastname}`
},
},
methods: {
getRandom() {
return Math.random()
},
hi() {
alert(this.fullname.toLowerCase())
alert(this.getRandom())
},
test() {
const fullname = this.fullname
const propE = this.propE
type cases = [
Expect<Equal<typeof fullname, string>>,
Expect<Equal<typeof propE, string | number>>,
]
},
},
})
在上面的例子中,使用了TypeScript 3.9中引入的一个新特性——// @ts-expect-error注释。把它放在代码前面,TypeScript就会忽略这个错误。如果代码中没有错误,TypeScript编译器会指出代码中有一个没有使用的指令(@ts-expect-error)。
另外,本例中还使用了 Expect
、 Equal
和 IsAny
实用程序类型,相关代码如下:
type Expect<T extends true> = T
type Equal<X, Y> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false
type IsAny<T> = 0 extends (1 & T) ? true : false
type Debug<T> = { [K in keyof T]: T[K] }
解决方案
由于 VueBasicProps
函数为原 SimpleVue
函数的 options
形参对象添加了一个新的 props
属性,因此我们引入了一个新的类型形参 Props
来表示其类型,并使用 extends
关键字来约束该类型形参的类型。
declare function VueBasicProps<
Data extends Record<string, any>,
Computed extends Record<PropertyKey, (...args: unknown[]) => unknown>,
Methods extends Record<PropertyKey, (...args: unknown[]) => unknown>,
Props extends Record<string, any>
>(options: {
data: (this: void) => Data,
computed: Computed & ThisType<Data>,
methods: Methods & ThisType<
& Data
& Methods
& {
[P in keyof Computed]: ReturnType<Computed[P]>
}>,
props: Props
}): any
在类型挑战描述中,要求注入的 props
属性在所有上下文中都是可访问的,包括 data
、 computed
和 methods
。为了访问 data
函数中 props
对象的属性,我们需要修改 data
函数的类型签名。也就是说,我们需要将 this: void
改为 this: Props
。之后,在示例中的 data
函数中定义的 PropsType
类型将生成以下类型:
type PropsType = {
propA: {};
propB: {
type: StringConstructor;
};
propC: {
type: BooleanConstructor;
};
propD: {
type: typeof ClassA;
};
propE: {
type: (StringConstructor | NumberConstructor)[];
};
propF: RegExpConstructor;
}
显然,此时的 PropsType
类型不能满足以下所有测试用例:
type cases = [
Expect<IsAny<PropsType["propA"]>>, // ❌
Expect<Equal<PropsType["propB"], string>>, // ❌
Expect<Equal<PropsType["propC"], boolean>>, // ❌
Expect<Equal<PropsType["propD"], ClassA>>, // ❌
Expect<Equal<PropsType["propE"], string | number>>, // ❌
Expect<Equal<PropsType["propF"], RegExp>> // ❌
]
为了满足上述测试用例,我们需要修改 data
函数中 this
“伪参数”的类型。因此,我们需要定义一个 PropsTypes
泛型来执行类型转换,但您也可以使用其他名称。
type PropsTypes<T> = {
[P in keyof T]: T[P]
}
在前面的PropsType
类型中,我们看到了 StringConstructor
、 BooleanConstructor
和 NumberConstructor
类型,它们是在TypeScript模块中的lib/lib.es5.d.ts文件中定义的:
// lib/lib.es5.d.ts
interface StringConstructor {
new(value?: any): String; // construct signature
(value?: any): string; // call signature
readonly prototype: String;
fromCharCode(...codes: number[]): string;
}
interface BooleanConstructor {
new(value?: any): Boolean; // construct signature
<T>(value?: T): boolean; // call signature
readonly prototype: Boolean;
}
interface NumberConstructor {
new(value?: any): Number; // construct signature
(value?: any): number; // call signature
readonly prototype: Number;
// omit other properties
}
interface RegExpConstructor {
new(pattern: RegExp | string): RegExp;
new(pattern: string, flags?: string): RegExp;
(pattern: RegExp | string): RegExp;
(pattern: string, flags?: string): RegExp;
readonly prototype: RegExp;
// omit other properties
}
从上面的代码可以看出,StringConstructor
、BooleanConstructor
和 NumberConstructor
类型呼叫签名的返回类型是与该类型对应的基本数据类型。那么我们如何在上述类型中提取调用签名的返回值类型呢?这就是我们需要使用条件类型和 infer
类型推断的地方,如下面的代码所示:
type ResultType<T> =
T extends { (value?: any): infer R }
? R : any
type R0 = ResultType<StringConstructor> // string
type R1 = ResultType<BooleanConstructor> // boolean
type R2 = ResultType<NumberConstructor> // number
type R3 = ResultType<RegExpConstructor> // RegExp
type R4 = ResultType<typeof ClassA> // any
在上面的代码中,我们定义了一个新的 ResultType
泛型。这个泛型已经正确地提取了 *Constructor
类型中调用签名的返回类型。但是 typeof ClassA
构造函数类型的返回类型还不能正确提取。
要解决这个问题,我们需要运用施工签名的知识。如果您不了解调用签名和构造签名,可以阅读下面的文章(注意,直接点击会报错,需要用右键复制链接地址到浏览器地址中打开)。
关于TypeScript Interface你需要知道的10件事TypeScript接口的10种使用场景——可能只有20%的web开发人员完全掌握它们https://mp.weixin.qq.com/s?__biz=MzU3NjM0NjY0OQ==&mid=2247484948&idx=1&sn=0cca3d731c2157286ca018938b9a16c8&chksm=fd140962ca638074085b7715cbf3d460c6d0579cd7c9c47ce5b5fab6e26e2571cd32199cc1ba&token=1779636375&lang=zh_CN#rd
type ResultType<T> =
T extends { (value?: any): infer R }
? R : T extends { new(...args: unknown[]): infer I }
? I : any
type R0 = ResultType<StringConstructor> // string
type R1 = ResultType<BooleanConstructor> // boolean
type R2 = ResultType<NumberConstructor> // number
type R3 = ResultType<RegExpConstructor> // RegExp
type R4 = ResultType<typeof ClassA> // ClassA
从上面的结果中,我们可以看到我们的 ResultType
泛型已经可以正确地提取类型。接下来,让我们更新前面定义的 PropsTypes
泛型:
type PropsTypes<T> = {
[P in keyof T]: ResultType<T[P]>
}
使用 PropsTypes
泛型,我们可以再次修改 data
函数中 this
“伪形参”的类型:
修改 this
“伪参数”类型后,已通过2个测试用例。
type cases = [
Expect<IsAny<PropsType["propA"]>>, // ✅
Expect<Equal<PropsType["propB"], string>>, // ❌
Expect<Equal<PropsType["propC"], boolean>>, // ❌
Expect<Equal<PropsType["propD"], ClassA>>, // ❌
Expect<Equal<PropsType["propE"], string | number>>, // ❌
Expect<Equal<PropsType["propF"], RegExp>> // ✅
]
为什么其他测试用例会失败?这是因为在这些失败的测试用例中使用的属性类型是由具有 type
属性的对象类型定义的,该属性指定当前 prop
的类型。
type PropsType = {
propA: {};
propB: {
type: StringConstructor;
};
propC: {
type: BooleanConstructor;
};
propD: {
type: typeof ClassA;
};
propE: {
type: (StringConstructor | NumberConstructor)[];
};
propF: RegExpConstructor;
}
在上述属性中, propE
属性的特殊之处在于,该属性的类型是数组类型,并且数组中每一项的类型都是 StringConstructor | NumberConstructor
联合类型。要获取数组项的类型,可以使用索引访问类型:
type R5 = (StringConstructor | NumberConstructor)[][number]
// StringConstructor | NumberConstructor
通过以上分析,我们可以看到,在更新 PropsTypes
泛型类型时,我们应该考虑对象类型和数组类型的情况。为了区分这两种类型,我们需要在TypeScript中使用条件类型,它们在下面的代码中实现:
type PropsTypes<T> = {
[P in keyof T]:
T[P] extends { type: infer R } // Get the prop type
? R extends readonly unknown[] // Determine whether it is an array type
? ResultType<R[number]> // Handle union types
: ResultType<R> // Handle a single type
: ResultType<T[P]>
}
在更新 PropsTypes
泛型类型之后,示例中 data
函数中的所有测试用例都正确通过。然而, VueBasicProps
函数的类型声明还没有完成,因为 props
属性还不能通过 computed
和 methods
属性中的函数内部的 this
上下文来访问,所以我们需要改变 computed
和 methods
属性的类型。
computed: Computed & ThisType<Data & PropsTypes<Props>>,
methods: Methods & ThisType<
& Data
& Methods
& PropsTypes<Props>
& {
[P in keyof Computed]: ReturnType<Computed[P]>
}>,
最后,让我们看一下完整的代码:
type PropsTypes<T> = {
[P in keyof T]:
T[P] extends { type: infer R }
? R extends readonly unknown[]
? ResultType<R[number]>
: ResultType<R>
: ResultType<T[P]>
}
type ResultType<T> =
T extends { (value?: any): infer R }
? R : T extends { new(...args: unknown[]): infer I }
? I : any
declare function VueBasicProps<
Data extends Record<string, any>,
Computed extends Record<PropertyKey, (...args: unknown[]) => unknown>,
Methods extends Record<PropertyKey, (...args: unknown[]) => unknown>,
Props extends Record<string, any>
>(options: {
data: (this: PropsTypes<Props>) => Data,
computed: Computed & ThisType<Data & PropsTypes<Props>>,
methods: Methods & ThisType<
& Data
& Methods
& PropsTypes<Props>
& {
[P in keyof Computed]: ReturnType<Computed[P]>
}>,
props: Props
}): any
在本文中,除了TypeScript映射类型、条件类型和 infer
类型推断外,我们还使用调用签名和构造函数签名来完成 VueBasicProps
函数类型定义过程。
在阅读本文之后,我相信您已经知道如何确定类型是否是构造函数类型。关于 ThisType<Type>
内置泛型角色的更多信息,你可以阅读“TypeScript泛型的高级用法第1部分”文章。而 PropsTypes
和 ResultType
泛型在内部使用多个条件类型,也称为条件链,如果您感兴趣,可以阅读下面的文章(注意,直接点击会报错,需要用右键复制链接地址到浏览器地址中打开)。