【问题标题】:Disable allowing assigning Readonly types to non-readonly types禁用允许将只读类型分配给非只读类型
【发布时间】:2021-07-11 23:57:44
【问题描述】:

我一直在查看 typescript 中的只读类型。可悲的是,它没有像我希望的那样工作。例如,看下面的代码:

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


【解决方案1】:

啊,您遇到了一个让某人非常恼火的问题,以至于有人用令人难忘的标题"readonly modifiers are a joke" 提交了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

使用起来很烦人,但绝对体现了只读的精神(只读??‍♂️),你不能不小心写到只读的东西。


另一个解决方法是运行一个只接受可变值的辅助函数,因为@TitianCernicova-Dragomirsuggested。可能是这样的:

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&lt;T&gt;,但mutable 函数将只接受已经可变的值。您必须记住在您希望是可变的任何值上调用mutable()。这很容易出错,所以我真的不推荐这种方法。


我也尝试过制作一个假的Readonly&lt;T&gt; 类型,它修改了T,以便将structurallyT 区分开来,但它和getter-function 方法一样麻烦.问题是,假设您希望能够将可变值分配给只读变量,但又希望阻止将只读值分配给可变变量,那么 readonly 修饰符需要扩大T 的类型,而不是缩小它。这将选项限制为Readonly&lt;T&gt; = {[K in keyof T]: T[K] | Something}Readonly&lt;T&gt; = T | Something。但在每种情况下,实际读取只读属性都变得非常困难,因为您必须缩小类型。如果您每次读取属性时都需要样板文件,那么您不妨使用 getter 函数。所以,算了吧。


总结一下:如果你真的想强制执行无法写入的属性,我认为 getter 函数方法可能是你最好的选择。或者,也许您应该放弃 readonly 修饰符,因为它们毕竟是个笑话?。希望有帮助。祝你好运!

【讨论】:

  • 这正是我想知道的。谢谢你非常详细的回答:)
  • ["OOPS", T, "has readonly properties"] 是改进 Typescript 错误消息的一种优秀方法。 (我想除了Tstring[] 之类的类型,在这种情况下,结果可分配给T,您根本不会收到错误消息。)
【解决方案2】:

这是我的绝招。

我向Mutable 类型添加了一个符号属性,这样就不能为其分配非Mutable 值。有意不从具有Mutable 定义的文件中导出符号,因此不通过makeMutable 函数就不可能创建有效的Mutable 对象。

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

如果您将可变值分配给只读变量,则存在潜在的错误来源,因为有权访问原始可变值的代码可能会意外地改变它。但我认为这是一个正交问题。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-03-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-28
    相关资源
    最近更新 更多