禁用允许将只读类型分配给非只读类型

问题描述 投票:0回答:3

我一直在研究打字稿中的只读类型。可悲的是,它并没有像我希望的那样工作。例如,请参阅下面的代码:

interface User{
    firstName: string;
    lastName: string;
}

const user: Readonly<User> = {
    firstName: "Joe",
    lastName: "Bob",
};

const mutableUser: User = user; //Is it possible to disallow this?

user.firstName = "Foo" //Throws error as expected
mutableUser.firstName ="Bar"//This works

是否可以以某种方式使用只读类型,但不允许将其分配给另一种非只读类型?如果不行的话可以用其他方法解决吗?

typescript immutability
3个回答
44
投票

啊,你遇到了一个让某人恼火的问题,他提交了一个 issue,其标题令人难忘 “只读修饰符是个笑话”(此后已更改为更中性的内容)。该问题正在 Microsoft/TypeScript#13347 上进行跟踪,但似乎没有太多进展。现在,我们只需要处理以下事实:

readonly
属性不会影响可分配性。

那么,有哪些可能的解决方法?


最干净的方法是放弃

readonly
属性,转而使用某种映射,通过 getter 函数等方式将对象变成只能读取的对象。例如,如果将只读属性替换为返回所需值的函数:

function readonly<T extends object>(x: T): { readonly [K in keyof T]: () => T[K] } {
  const ret = {} as { [K in keyof T]: () => T[K] };
  (Object.keys(x) as Array<keyof T>).forEach(k => ret[k] = () => x[k]);
  return ret;
}

const user = readonly({
  firstName: "Joe",
  lastName: "Bob",
});

const mutableUser: User = user; // error, user is wrong shape

// reading from a readonly thing is a bit annoying
const firstName = user.firstName();
const lastName = user.lastName();

// but you can't write to it
user.firstName = "Foo" // doesn't even make sense, "Foo" is not a function
user.firstName = () => "Foo" // doesn't work because readonly

或者类似地,如果只读对象仅公开单个 getter 函数:

function readonly<T extends object>(x: T): { get<K extends keyof T>(k: K): T[K] } {
  return { get<K extends keyof T>(k: K) { return x[k] } };
}

const user = readonly({
  firstName: "Joe",
  lastName: "Bob",
});

const mutableUser: User = user; // error, user is wrong shape

// reading from a readonly thing is a bit annoying
const firstName = user.get("firstName");
const lastName = user.get("lastName");

// but you can't write to it
user.firstName = "Foo" // doesn't even make sense, firstName not a property

使用起来很烦人,但绝对强化了只读性的精神(只读性?🤷u200d♂️),你不能意外地写入只读的东西。


另一个解决方法是运行一个仅接受可变值的辅助函数,如 @TitianCernicova-Dragomir 建议。可能是这样的:

type IfEquals<T, U, Y = unknown, N = never> =
  (<V>() => V extends T ? 1 : 2) extends
  (<V>() => V extends U ? 1 : 2) ? Y : N;
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type IsMutable<T, Y=unknown, N=never> = IfEquals<T, Mutable<T>, Y, N>

const readonly = <T>(x: T): Readonly<T> => x;
const mutable = <T>(
  x: T & IsMutable<T, unknown, ["OOPS", T, "has readonly properties"]>
): Mutable<T> => x;

const readonlyUser = readonly({
  firstName: "Joe",
  lastName: "Bob",
});
const mutableUser = mutable(
  { firstName: "Bob", lastName: "Joe" }
); // okay

const fails: User = mutable(readonlyUser); // error, can't turn readonly to mutable
// msg includes ["OOPS", Readonly<{ firstName: string; lastName: string; }>
// , "has readonly properties"]

const works = readonly(mutableUser); //okay, can turn mutable to readonly

这里

readonly
函数将接受
T
类型的任何值并返回
Readonly<T>
,但
mutable
函数将只接受已经可变的值。您必须记住对您希望可变的任何值调用
mutable()
。这很容易出错,所以我真的不推荐这种方法。


我还尝试过制作一个假的

Readonly<T>
类型,该类型修改了
T
,以便在结构上T
 区分开来,但它与 getter 函数方法一样麻烦。问题是,假设您希望能够将可变值分配给只读变量,但希望阻止将只读值分配给可变变量,则 readonly 修饰符需要扩大 
T
 的类型,而不是缩小它。这将选项限制为 
Readonly<T> = {[K in keyof T]: T[K] | Something}
Readonly<T> = T | Something
 之类的内容。但在每种情况下,实际读取只读属性都变得非常困难,因为您必须缩小类型范围。如果每次读取属性时都需要样板,那么不妨使用 getter 函数。所以,忘记吧。


总结一下:如果您确实想强制执行无法写入的属性,我认为 getter 函数方法可能是您最好的选择。或者也许你应该放弃

readonly

 修饰符,因为它们毕竟是一个笑话 🤡.


0
投票
这是我的破解。

我向

Mutable

 类型添加了一个符号属性,以便无法为其分配非 
Mutable
 值。该符号故意不从具有 
Mutable
 定义的文件中导出,因此如果不通过 
Mutable
 函数就无法创建有效的 
makeMutable
 对象。

在文件 Mutable.ts 中:

const mutableMarker = Symbol("mutable marker"); type MutableMarker = typeof mutableMarker; export type Mutable<T> = { -readonly [P in keyof T]: T[P]; } & { [mutableMarker]: MutableMarker; }; export function makeMutable<T>(value: T): Mutable<T> { return { ...value, [mutableMarker]: mutableMarker }; }
用途:

// Everything should be readonly by default. interface User { readonly firstName: string; readonly lastName: string; } // This is normal. const readonlyUser: User = { firstName: "Joe", lastName: "Bob", }; // Error. Can't assign to readonly property. readonlyUser.firstName = ""; // This creates a mutable copy, so the original object won't be modified. const mutableUser = makeMutable(readonlyUser); mutableUser.firstName = ""; // This is fine. A readonly variable can be assigned the mutable value. const works: User = mutableUser; // Error. Can't assign to readonly property. works.firstName = ""; // Error. Property '[mutableMarker]' is missing in type 'User'. const fails: Mutable<User> = readonlyUser;
如果将可变值分配给只读变量,则存在潜在的错误来源,因为有权访问原始可变值的代码可能会意外地改变它。但我认为这是一个正交问题。


0
投票
很多答案都暗示您必须使用函数包装器。实际上,您不必使用受歧视工会的力量。

如果您使用具有相同键但类型不同的属性“标记”每个属性,则 TypeScript 会抱怨。

所以,如果你有这个:

interface User { __readonly?: false firstName: string; lastName: string; } type ReadOnlyUser = Omit<Readonly<User>, '__readonly'> & { __readonly?: true } const user: ReadOnlyUser = { firstName: "Joe", lastName: "Bob", }; const mutableUser: User = user; //Is it possible to disallow this? user.firstName = "Foo" //Throws error as expected mutableUser.firstName ="Bar" //This works
然后,您应该会看到如下 TypeScript 错误:

© www.soinside.com 2019 - 2024. All rights reserved.