TypeScript类型声明高级用法
最近看了一些分析TypeScript
的文章,发现有很多自己不了解的地方,原来类型声明还有这么多高级用法,真是有点落伍了。于是重新补习了一下TS文档,整理了本篇文章。
参考
- 官网文档,本文大部分示例代码均来自于官方文档,添加了一些js代码和相关注释
- typescript不能不掌握的高级特性
混合类型
JavaScript
运行函数代码额外的属性和方法,参考:混合类型
比如下面这个方法
function counter(start){}
counter.interval = 123
counter.reset = function(){}
如果要对counter类型进行声明,就需要用到混合类型
interface Counter{
(start:number): void;
interval:number;
reset:()=>void;
}
function getCounter():Counter{
// 首先使用强制类型转换声明counter变量的类型
// let counter = <Counter>function(start:number){}
let counter = <Counter>((start:number)=>{}) // 后面的函数声明必须要加括号,表示<Counter>强制类型转换的是后面整个函数表达式
counter.interval = 123
counter.reset = function(){}
return counter
}
泛型
泛型可以帮助我们捕获用户传入的类型,换言之,我们定义的类型可以兼容未来用户指定的类型。
基本的用法如下
type Value<T> = T
type NumberValue = Value<number> // number
type StringValue = Value<string> // string
let s1: StringValue = 'hello'
// let s2: StringValue = 1 // type error
下面是一个泛型函数
function identity<T>(arg: T): T {
return arg;
}
let a = identity(123) // a的类型为number
我们在声明类型的时候,也可以使用泛型类型。比如上面泛型函数identity
本身的泛型类型
// 等价于
type IdentityType1 = typeof identity
// 对象字面量的调用签名,可以参考上面的混合类型
type IdentityType2 = {
<T>(arg: T): T
}
type IdentityType3 = <T>(arg: T)=> T;
// IdentityType1、IdentityType2、IdentityType3 三种类型是相同的
let myIdentity:IdentityType1 = identity
如果我们想要为myIdentity
函数的参数限定某个类型,则可以使用泛型接口
interface GenericIdentityFn<T> {
(arg: T): T; // 跟上面IdentityType2的区别在于不再是描述函数泛型,而是描述泛型接口
}
let myIdentity: GenericIdentityFn<number> = identity; // 限定只能接收number
myIdentity(123)
identity("hello") // 不会影响identity
// myIdentity("hello") // error
let myIdentity2: GenericIdentityFn<string> = identity;
myIdentity2("hello")
同理,也可以创建泛型类
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let a = new GenericNumber<number>() // 在构造对象时才指定具体类型
a.add(100, 200)
// a.add('1', '2')// error
有时候虽然我们使用了泛型,但是我们希望用户只传入一些按照约定的类型,而非所有类型,此时可以使用泛型约束,使用T extends type
的语法声明泛型约束
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // 正常
return arg;
}
loggingIdentity(123) // error,参数类型T必须包含length属性
一种特殊的约束是我们希望参数是某个类,这时候需要需要使用new()
function create<T>(c: {new(): T; }): T {
return new c();
}
function a(){}
class A{}
// create(a) // error
create(A) // right
交叉类型 &
在JavaScript中一种常见的场景是合并两个对象,如$.fn.extend
、Object.assign
等,这种场景下要求这些方法返回的是两个参数合并后的类型,因此需要使用交叉类型&
function extend<T, U>(first: T, second: U): T & U {
let result = <T & U>{};
for (let id in first) {
(<any>result)[id] = (<any>first)[id];
}
for (let id in second) {
if (!result.hasOwnProperty(id)) {
(<any>result)[id] = (<any>second)[id];
}
}
return result;
}
class Person {
constructor(public name: string) { }
}
class ConsoleLogger {
log() {}
}
var jim = extend(new Person("Jim"), new ConsoleLogger());
var n = jim.name;
jim.log();
// jim.xxx // error
联合类型 |
如果某个函数希望传入的参数是某几种指定类型中的一种,可以使用联合类型
type padding = number | string
注意如果某个类型是联合类型时,由于在运行时该变量究竟是哪一种类型,我们只能访问此联合类型的所有类型里共有的成员
interface Bird {
fly();
layEggs();
}
interface Fish {
swim();
layEggs();
}
function getSmallPet(type: number = 1): Fish | Bird {
let obj = {}
return type === 1 ? <Fish>obj : <Bird>obj
}
let pet = getSmallPet();
pet.layEggs(); // okay
// pet.swim(); // errors
字面量类型
枚举类型在使用时需要通过EnumType.EnumValue
的方式进行,字面量类型可以实现类似的功能,并借助TS的类型检测机制避免出现“魔法变量”
type Easing = "ease-in" | "ease-out" | "ease-in-out";
function animate(type:Easing):void{}
// animate(123)// error
// animate('ease-xx')// errror
animate('ease-in') // 只能是指定的字符串
目前支持字符串字面量类型和数字字面量类型,字面量类型也被称为单例类型。
索引类型
在JavaScript中动态访问对象属性是一种很常见的操作,比如下面方法返回某个对象指定属性名的值列表
function pluck(o, names) {
return names.map((n) => o[n]);
}
let o = { x: 1, y: 2, z: 3 };
let ans = pluck(o, ["x", "y"]); // [1, 2]
如何通过ts确定ans的类型呢?如何限定names的取值仅限于对象O上存在的属性列表呢?此时就可以用到索引类型。
function pluck<T, K extends keyof T>(o:T, names:K[]):T[K][] {
return names.map((n) => o[n]);
}
let o = { x: 1, y: '2', z: true };
let ans = pluck(o, ["x", "y"]); // ans的类型为 (string | number)[]
pluck(o, ["x", 'xxx']); // error
这里用到了几个知识点
keyof
获取类型T上已知的公共属性名的联合类型T[K]
是索引访问操作符,类似于o['x']
,不过这里返回的是类型,这种类型被称为索引签名
其中索引签名直接获取某个类型下单个属性的类型,例如
type Test = {
foo: number;
bar: string
}
type N = Test['foo'] // number
条件类型
在TS v2.8引入了条件类型,能够表示非统一的类型
type IsNumber<T> = T extends number ? 'yes' : 'no';
type A = IsNumber<2> // yes
type B = IsNumber<'3'> // no
let a1: A = 'yes' // yes
这是一个非常强大的特性
高级用法
动态获取类型 typeof
typeof 获取类型,一种用法是获取类的别名,在我们需要动态地将某个类型赋值给新的变量时很有用
class Greeter{}
let greetMaker : typeof Greeter = Greeter // greetMaker的类型是 Greeter
遍历属性类型 keyof
在JavaScript中动态获取属性是非常常见的
function getProperty(obj, key) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
let a = getProperty(x, 'a')// a 的类型是any,无法对key参数进行约束,因此无法检测是否传入了错误的key
这个时候可以使用keyof
,这是用于获取某个类型的属性类型
function getProperty<T>(obj: T, key: keyof T) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a"); // okay
// getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.
infer
infer
表示条件类型中的类型推断 ,必须在条件类型中出现。可以理解为在声明类型中的占位符,在后面类型推断时才确定具体类型
type GetParent<T> = T extends infer R ? R: never
type MyNumber = GetParent<number> // MyNumber = number
// 计算逻辑 type Get<number> = number extends infer number ? number: never
下面是一个获取函数参数列表类型Parameters
的例子
type TArea = (width: number, height: number) => number;
// Parameters是ts内置类型方法,用于获取参数列表类型
type params = Parameters<TArea>; // params类型为[number, number]
我们可以手动实现Parameters
type Parameters2<T extends (...args: any) => any> = T extends ((...args: infer P) => any ) ? P : never;
// 需要理解的是因为Parameters2计算的是函数参数类型,所以其泛型约束是一个函数
// 在这函数类型约束里面通过infer P 占位,然后就可以获取参数类型了
type params2 = Parameters2<TArea>;
同理,我们可以实现一个ReturnType
type returnTypes = ReturnType<TArea> // 内置
type ReturnType2<T extends (...args: any) => any> = T extends ((...args: any) => infer R) ? R : any;
type returnTypes2 = ReturnType2<TArea>
综合:Vue3中的ref
参考
ref是Vue3中新出现的一个类型。Vue3通过Proxy代理了对象的set和get等方法,从而实现响应式数据;但是对于基础类型而言,则需要通过Ref
进行包装才能实现类似的功能。
const count:Ref = ref(2)
count.value // 获取count的值
count.value = 3 // 更新count的值
Ref类型的特殊在于
- Ref类型可以嵌套,
let a = ref(ref(ref(2)))
返回的也是Ref类型,因此可以直接通过a.value
访问到具体的值而无需使用a.value.value.value
- 对象属性值可以是Ref类型,
let a = ref({x:ref(2)})
,却可以直接通过a.value.x
访问到x
那么是如何保证typescript在编译时类型推断正确的呢?这一章节就来研究如何声明Ref
类型。
首先是最简单的方式
function demo1() {
// 这里用到了泛型的默认值语法 <T = any>
type Ref<T = any> = {
value: T
}
// 只看定义,先忽略实现,vscode依旧会帮助我们进行类型推断
function ref<T>(value: T): Ref<T>
// 基础用法
let a = ref(1) // a的类型 Ref<number>
let b = ref(ref(1)) // b的类型变成了Ref<Ref<number>>,我们希望得到原始的 Ref<number>而不是嵌套
}
然后实现Ref解包,可以通过使用类型约束和条件类型
function demo2() {
type Ref<T = any> = {
value: T
}
function ref<T>(value: T): T extends Ref ? T : Ref<T>
let a = ref(ref(1)) // Ref<number>
let b = ref(ref(ref(1))) // Ref<number>
let c = ref(ref(ref({ x: 100 }))) // Ref<{x: number;}>
let d = ref({ x: ref(1) }) // Ref<{x: Ref<number>;}> 然而我们希望对属性值也进行解包,得到 Ref<{x: number;}>
d.value.x // Ref<{x: number;}>
}
然后处理属性为Ref类型的对象参数,在这一步我们借助infer
实现UnwrapRef
function demo3() {
type Ref<T = any> = {
value: T
}
type UnwrapRef<T> = T extends Ref<infer R> ? R : T
type x = UnwrapRef<Ref<number>> // number
function ref<T>(value: T): T extends Ref ? T : Ref<UnwrapRef<T>>
let d = ref({ x: ref(1) }) // 很可惜,做完这一步, d.value.x的类型还是Ref<number>,因为infer R中并没有判断处理R为Ref的情况,貌似是一个递归问题了
d.value.x
}
然后处理infer R
为Ref类型的情况
function demo4() {
type Ref<T = any> = {
value: T
}
// 由于TS不支持类型声明递归,可以通过下面这种取巧的方式实现
// type UnwrapRef<T> = T extends Ref<infer R> ? UnwrapRef<R> : T // 这么写会报错
type UnwrapRef<T> = {
ref: T extends Ref<infer R> ? UnwrapRef<R> : T,
other: T
}[T extends Ref ? 'ref' : 'other'] // 绕开上面限制,使用索引签名
type a = UnwrapRef<Ref<number>> // number
// 首先 T extends Ref ,获取'ref'索引,
// 然后 T extends Ref<infer R> ? UnwrapRef<R> : T,返回R类型为number,继续UnwrapRef<number>
// T ex tends Ref 获取'ohter'索引,终止递归,返回类型a = number
function ref<T>(value: T): T extends Ref ? T : Ref<UnwrapRef<T>>
let d = ref({ x: ref(1) })
d.value.x // Ref<number> 我们还需要对Object的每个属性进行UnwrapRef
}
最后,将上面得到的递归方案运用在对象属性上
function demo5(){
type Ref<T = any> = {
value: T
}
type UnwrapRef<T> = {
ref: T extends Ref<infer R> ? UnwrapRef<R> : T,
object: { [K in keyof T]: UnwrapRef<T[K]> }, // 将对象的每一个属性进行解包
other: T
}[T extends Ref ? 'ref' : T extends Object ? 'object' : 'other'] // 绕开上面限制,使用索引签名
function ref<T>(value: T): T extends Ref ? T : Ref<UnwrapRef<T>>
let d = ref({ x: ref(1) }) // d的类型为 Ref<{x: number;}>,bingo,目标达成
}
至此,就实现了Vue3中Ref
类型的实现,可以看见类型声明的高级用法还是很有趣且有用的。
小结
不得不说使用TS开发虽然繁琐了一点,但确实很香,之前一直都是在自己的项目中小打小闹写写TS,没有在正式业务中使用,以至于TS编码水平比较低下,打算接下来在现有项目中逐步引入TypeScript开发~
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。