PostsAboutGames

TypeScript Readonly and Pick

April 28, 2019 - Søren Alsbjerg Hørup

An awesome feature of TypeScript is the ability to generate inferred types. C# supports this feature only for anonymous types, e.g.:

 var t = new { A = 1, B = "b" };

Creates a new object with an anonymous type having two members: A and B.

Similar can be done in TypeScript:

let t = {a:1, b:"b"};

Where t is also an anonymous type having two members: a and b.

TypeScript provides many advanced typing features that can be used to increase type safety.

An example are built-in generics such as Readonly and Pick.

Readonly is pretty simple, it takes a type and returns a new type where all properties are read only. Example:

let t = {a:1, b:"b"};
t.a = 2; // OK.

function makeReadonly<T>(t:T):Readonly<T>
{
   return t;
}

let tt = makeReadonly(t);
tt.a = 2; // not OK

The makeReadonly function simply takes a value of type T and returns the same value but changing its type to Readonly. This throws and error at compile time, thus stopping us from mutation the properties of the object.

Another great built-in generic is Pick. Pick allows us to construct a new type based upon the properties of another type.

Consider the following unsafe example, where we have a pick function that takes an object and returns a new object with only a single property taken from the original object.

let t = {a:1, b:"b"};
function pick<T>(t:T, property:string)
{
   let o = {} as any;
   o\[property\] = t\[property\];
   return o;
}
let o = pick(t, 'c'); // compiles, but c is undefined

Compiles but will fail at run-time since ‘c’ is not defined. This can be made much more type secure by introducing another generic K, which is a set of the keys of the object:

let t = {a:1, b:"b"};
function pick<T, K extends keyof T>(t:T, property:K)
{
   let o = {} as T;
   o\[property\] = t\[property\];
   return o;
}
let o = pick(t, 'c'); // no longer compiles!

Since c is not defined in T, the compiler throws an error. However, type safety is still not guarantee 100%. Consider this example:

let o = pick(t, 'a'); // compiles as expected.
let b = o.b;          // compiles, but b is not defined!

Since we picked ‘a’ from T, the compiler is OK on line 1. The compiler is also OK at line 2, since ‘b’ is defined in type T. However, this will fail at run-time due to ‘b’ not being defined. This can be fixed by using Pick!

let t = {a:1, b:"b"};
function pick<T, K extends keyof T>(t:T, property:K):Pick<T, K>
{
   let o = {} as Pick<T, K>;
   o\[property\] = t\[property\];
   return o;
}
let o = pick(t, 'a'); // compiles as expected.
let b = o.b;          // no longer compiles!

With the introduction of Pick, the function now returns a new Type containing a subset of type T. In the example above, we picked ‘a’ and thus generated a new type containing only ‘a’. The last nine now throws an error at compile time, since ‘b’ is no longer defined.

Using these built-in generics can in the end safe the day, with the added benefit of providing improved intellisense.