Typescript实现指定属性变成readonly
1. 存在的问题
typescript内置的Readonly类型只能为所有的属性加上readonly关键字,假设已经有如下的interface叫Circle:
interface Circle {
kind: "circle";
radius: number;
x: number;
y: number;
}
使用Readonly类型对其进行转换:
type ReadonlyCircle = Readonly<Circle>;
鼠标放到ReadonlyCircle上,vscode给出类型提示:
可以看到已经将Circle中的所有属性全部加上了readonly关键字,但是给所有属性都加上了
2. 如果只想给部分属性添加readonly关键字呢
type ReadonlyWhen<T, K extends keyof T> = Readonly<Pick<T, K>> & Omit<T, K>
这里还用到了typescript内置的另外两个类型:Pick和Omit
- Pick<T, K>:从类型T中取出K属性,K可以是联合属性,即K可以是"p1" | "p2" | "p3"这种;
- Omit<T, K>: 刚好和Pick相反,取出的是除K之外的其他属性;
这里首先取出了K这个属性,将其前面加上readonly关键字,然后再交叉上T中除K之外的其他属性。
看看效果:
可以看到我们写的ReadonlyWhen这个已经生效了,只是类型提示不够友好,试一下修改Kind6的kind属性会不会报错:
type ReadonlyWhen<T, K extends keyof T> = Readonly<Pick<T, K>> & Omit<T, K>
type Kind6 = ReadonlyWhen<Circle, "kind">
const kind6: Kind6 = {
kind: "circle",
radius: 1,
x: 1,
y: 1
}
kind6.kind = "circle"
kind6.radius = 2
联合类型也可以:
可以看到编辑器的类型提示已经生效了。
3. 优化
1. 初步优化
刚刚我们看到,这里的类型提示,还是很鸡肋,尽管ReadonlyWhen的写法已经很简单明了:
看一下官方的Readonly是怎么实现的:
模仿一下:
type ReadonlyWhen<T, K extends keyof T> = {
[P in keyof T as (P extends K ? never: P)]: T[P]
} & {
readonly [P in K]: T[P]
}
这里使用了never来过滤K属性(参考了Omit的实现),然后给K属性单独添加readonly关键字,最后交叉一下。
看一下效果:
type ReadonlyWhen<T, K extends keyof T = keyof T> = {
[P in keyof T as (P extends K ? never: P)]: T[P]
} & {
readonly [P in K]: T[P]
}
type Kind6 = ReadonlyWhen<Circle, "kind" | "radius">
可以看到,类型提示更加友好。
2. 再次优化
如果我们希望第二个参数K省略的时候,相当与内置的Readonly呢?
只需要在K的声明时,加上默认值即可(从K extends keyof T
变成了K extends keyof T = keyof T
):
type ReadonlyWhen<T, K extends keyof T = keyof T> = {
[P in keyof T as (P extends K ? never: P)]: T[P]
} & {
readonly [P in K]: T[P]
}
type Kind6 = ReadonlyWhen<Circle>
4. 实现Pick
type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}
type Kind1 = MyPick<Circle, "kind" | "radius">
4. 实现Omit
type MyOmit<T, K extends string | number | symbol> = {
[P in keyof T as Exclude<P, K>]: T[P]
}
type Kind2 = MyOmit<Circle, "kind">
4. 实现Partial
type MyPartial<T> = {
[P in keyof T]?: T[P] | undefined
}
type Kind4 = MyPartial<Circle>
5. 实现重命名指定属性(使用as关键字)
// type Replace<T, K1, K2> = T extends K1 ? K2 : T;
type Rename<T, K1 extends keyof T, K2 extends string> = {
// [P in keyof T as Replace<P, K1, K2>]: T[P]
// [P in keyof T as P extends K1 ? K2 : P]: T[P]
[P in keyof T as (P extends K1 ? K2 : P)]: T[P]
}
type Kind3 = Rename<Circle, "kind", "kind1">
6. 将所有属性做映射
这里使用了& string,来保证Capitalize中类型正确(因为它的类型约束是string):
type Getters<T> = {
[K in keyof T as `get${Capitalize<K & string>}`]: () => T[K]
};
interface Person {
name: string;
age: number;
location: string;
}
type LazyPerson = Getters<Person>;