Typescript实现指定属性变成readonly

发布时间 2023-04-10 18:16:02作者: pangqianjin

1. 存在的问题

typescript内置的Readonly类型只能为所有的属性加上readonly关键字,假设已经有如下的interface叫Circle:

interface Circle {
    kind: "circle";
    radius: number;
    x: number;
    y: number;
}

使用Readonly类型对其进行转换:

type ReadonlyCircle = Readonly<Circle>;

鼠标放到ReadonlyCircle上,vscode给出类型提示:

image.png

可以看到已经将Circle中的所有属性全部加上了readonly关键字,但是给所有属性都加上了

2. 如果只想给部分属性添加readonly关键字呢

type ReadonlyWhen<T, K extends keyof T> = Readonly<Pick<T, K>> & Omit<T, K>

这里还用到了typescript内置的另外两个类型:PickOmit

  1. Pick<T, K>:从类型T中取出K属性,K可以是联合属性,即K可以是"p1" | "p2" | "p3"这种;
  2. Omit<T, K>: 刚好和Pick相反,取出的是除K之外的其他属性;

这里首先取出了K这个属性,将其前面加上readonly关键字,然后再交叉上T中除K之外的其他属性。

看看效果:

image.png

可以看到我们写的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

image.png

联合类型也可以:
image.png

可以看到编辑器的类型提示已经生效了。

3. 优化

1. 初步优化

刚刚我们看到,这里的类型提示,还是很鸡肋,尽管ReadonlyWhen的写法已经很简单明了:
image.png

看一下官方的Readonly是怎么实现的:

image.png

模仿一下:

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">

image.png

image.png

可以看到,类型提示更加友好。

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>

image.png

4. 实现Pick

type MyPick<T, K extends keyof T> = {
    [P in K]: T[P]
}
type Kind1 = MyPick<Circle, "kind" | "radius">

image.png

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">

image.png

4. 实现Partial

type MyPartial<T> = {
    [P in keyof T]?: T[P] | undefined 
}
type Kind4 = MyPartial<Circle>

image.png

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">

image.png

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>;

image.png