readonly
TypeScript's type system allows you to mark individual properties on an interface as readonly
. This allows you to work in a functional way (unexpected mutation is bad):
function foo(config: {
readonly bar: number,
readonly bas: number
}) {
// ..
}
let config = { bar: 123, bas: 123 };
foo(config);
// You can be sure that `config` isn't changed 🌹
Of course you can use readonly
in interface
and type
definitions as well e.g.:
type Foo = {
readonly bar: number;
readonly bas: number;
}
// Initialization is okay
let foo: Foo = { bar: 123, bas: 456 };
// Mutation is not
foo.bar = 456; // Error: Left-hand side of assignment expression cannot be a constant or a read-only property
You can even declare a class property as readonly
. You can initialize them at the point of declaration or in the constructor as shown below:
class Foo {
readonly bar = 1; // OK
readonly baz: string;
constructor() {
this.baz = "hello"; // OK
}
}
Readonly
There is a type Readonly
that takes a type T
and marks all of its properties as readonly
using mapped types. Here is a demo that uses it in practice:
type Foo = {
bar: number;
bas: number;
}
type FooReadonly = Readonly<Foo>;
let foo:Foo = {bar: 123, bas: 456};
let fooReadonly:FooReadonly = {bar: 123, bas: 456};
foo.bar = 456; // Okay
fooReadonly.bar = 456; // ERROR: bar is readonly
Various Use Cases
ReactJS
One library that loves immutability is ReactJS, you could mark your Props
and State
to be immutable e.g.:
interface Props {
readonly foo: number;
}
interface State {
readonly bar: number;
}
export class Something extends React.Component<Props,State> {
someMethod() {
// You can rest assured no one is going to do
this.props.foo = 123; // ERROR: (props are immutable)
this.state.baz = 456; // ERROR: (one should use this.setState)
}
}
You do no need to however as the type definitions for React mark these as readonly
for you (by internally wrapping the passed in generic types with the Readonly
type mentioned above).
export class Something extends React.Component<{ foo: number }, { baz: number }> {
// You can rest assured no one is going to do
someMethod() {
this.props.foo = 123; // ERROR: (props are immutable)
this.state.baz = 456; // ERROR: (one should use this.setState)
}
}
Seamless Immutable
You can even mark index signatures as readonly:
/**
* Declaration
*/
interface Foo {
readonly[x: number]: number;
}
/**
* Usage
*/
let foo: Foo = { 0: 123, 2: 345 };
console.log(foo[0]); // Okay (reading)
foo[0] = 456; // Error (mutating): Readonly
This is great if you want to use native JavaScript arrays in an immutable fashion. In fact TypeScript ships with a ReadonlyArray<T>
interface to allow you to do just that:
let foo: ReadonlyArray<number> = [1, 2, 3];
console.log(foo[0]); // Okay
foo.push(4); // Error: `push` does not exist on ReadonlyArray as it mutates the array
foo = foo.concat([4]); // Okay: create a copy
Automatic Inference
In some cases the compiler can automatically infer a particular item to be readonly e.g. within a class if you have a property that only has a getter but no setter, it is assumed readonly e.g.:
class Person {
firstName: string = "John";
lastName: string = "Doe";
get fullName() {
return this.firstName + this.lastName;
}
}
const person = new Person();
console.log(person.fullName); // John Doe
person.fullName = "Dear Reader"; // Error! fullName is readonly
Difference from const
const
- is for a variable reference
- the variable cannot be reassigned to anything else.
readonly
is
- for a property
- the property can be modified because of aliasing
Sample explaining 1:
const foo = 123; // variable reference
var bar: {
readonly bar: number; // for property
}
Sample explaining 2:
let foo: {
readonly bar: number;
} = {
bar: 123
};
function iMutateFoo(foo: { bar: number }) {
foo.bar = 456;
}
iMutateFoo(foo); // The foo argument is aliased by the foo parameter
console.log(foo.bar); // 456!
Basically readonly
ensures that cannot be modified by me, but if you give it to someone that doesn't have that guarantee (allowed for type compatibility reasons) they can modify it. Of course if iMutateFoo
said that they do not mutate foo.bar
the compiler would correctly flag it as an error as shown:
interface Foo {
readonly bar: number;
}
let foo: Foo = {
bar: 123
};
function iTakeFoo(foo: Foo) {
foo.bar = 456; // Error! bar is readonly
}
iTakeFoo(foo); // The foo argument is aliased by the foo parameter