Monday, November 25, 2019

TypeScript Recipe: Type Transformations in Practice

Compile-time errors are the best help to avoid mistakes during writing the code and prevent potential runtime-errors

So let's squeeze some juice from Ts Mapped types

Index type query operator keyof T

Index type query operator keyof T provides you the union of public property names of T.

TypeScript
type OptionKeys = 'option1' | 'option2' // union of possible options
type OptionFlags = { [K in Keys]: boolean }

OptionFlags is equivalent yo the following type:

TypeScript
type OptionFlags = {
  option1: boolean;
  option2: boolean;
}

Now let's use keyof T to produce OptionKeys out of it:

TypeScript
type OptionKeys = keyof OptionFlags // same as union 'option1' | 'option2'

We could introduce generic for any T:

TypeScript
type KeysUnion<T> = keyof T;
type OptionKeys = KeysUnion<OptionFlags> // same as union 'option1' | 'option2'

Imagine we're expecting some option change now:

TypeScript
let selectedOption: KeysUnion<OptionFlags> = 'option1'; // OK
selectedOption = 'unknownOption' // Err: Type "unknownOption" is not assignable to type "'option1' | 'option2'"

Describe type with index type typeof keyof T

If we have already instantiated object or enum we could use typeof to describe type.

TypeScript
const options = {
  option1: true,
  option2: false
}

type OptionKeys = keyof typeof options;
let selectedOption: OptionKeys = 'option2';
selectedOption = 'unknownOption' // Err: Type "unknownOption" is not assignable to type "'option1' | 'option2'"

Mapped Types

Transform the properties of existing type in some way to improve your code. You could find the syntax here Advanced Types – mapped types

Note: Following operators are homomorphic – the compiler copies all the existing property modifiers before adding any new ones.

Partial<T> – make all properties optional

TypeScript
type Partial<T> = {
    [P in keyof T]?: T[P];
}

Very handy with incremental updates.

TypeScript
type OptionsUpdate = Partial<Options>
// {
//   option1?: boolean;
//   option2?: boolean;
// }

Incremental update:

TypeScript
const update: Partial<Options> = {
  option2: true
}
const currentOptions: Options = { ...defaults, ...update }; // OK

Required<T> – make all properties required

TypeScript
type Required<T> = {
    [P in keyof T]-?: T[P];
}

Opposite to Partial<T>Required<T> is handy if you need to make all options mandatory.

TypeScript
type OptionsDefault = Required<Options>
// {
//   option1: boolean;
//   option2: boolean;
// }

Compiler help example:

TypeScript
const defaults: Required<Options> = { // Error: property 'option2' is missing
  option1: true
}

Readonly<T> – make all properties readonly (immutable)

TypeScript
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
}
----

Immutability is the brother of your code safety.

TypeScript
type ImmutableOptions = Readonly<Options>
// {
//   readonly option1: boolean;
//   readonly option2: boolean;
// }

Compiler help example:

TypeScript
const snapshot: Readonly<Options> = {
  option1: true,
  option2: false
}
snapshot.option2 = true; // Error: Cannot assign to 'option2' because it is a read-only property

Nullable<T> – make all properties nullable

TypeScript
type Nullable<T> = {
  [P in keyof T]: T[P] | null
}
TypeScript
type OptionLabels = Nullable<StringOptions>
// {
//   option1: string | null;
//   option2: string | null;
// }

Compiler help example:

TypeScript
const snapshot: Nullable<StringOptions> = {
  option1: 'priority',
  option2: 'on'
}
snapshot.option2 = 0; // Error: 0 is not assignable to type 'string | null'

Mutable<T> – make all properties mutable

TypeScript
type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
}

I recommend to not convert any immutable stuff to mutable, yet it still might be useful in some low level optimizations.

Pick<T,K> – picks the set K of properties from T

So it narrows the type to the properties we pick:

TypeScript
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
}

Useful for full items/cards preview sub types.

TypeScript
interface Todo {
    title: string;
    description: string;
    priority: string;
    completed: boolean;
}

type TodoPreview = Pick<Todo, 'title' | 'completed'>;
// {
//   title: string;
//   completed: boolean;
// }

You could achieve the same result via opposite type Omit<T,U>. Which picking all properties from T and then removes set of properties K.

TypeScript
type TodoPreview = Omit<Todo, 'description'>;

NonNullable<T> – excludes null and undefined from T

TypeScript
type AvailableOption = NonNullable<string | number | null | undefined>;  // string | number

Conclusion

The better type declarations you have the safer you are in your development and consequently in production.


Bonus

Describe type from String Enum with [P in T]

It's frequent case when particular features might be turned on/off
and we need to know them all but at the same time to be able to check each one by name.

TypeScript
enum Feature {
  Multiplayer = 'multiplayer',
  Autosave = 'autosave'
}

type Capabilities = {
  readonly [P in Feature]?: boolean;
}

const obj: Capabilities = {
  multiplayer: true,
  autosave: true,
  unknown: false // Error: 'unknown' does not exist in type 'Capabilities'
}

Describe Union type from Array of Strings / String Enum values

TypeScript
const CryptoCodes = <const>['BTC', 'ETH', 'LTC', 'DOGE']

type CryptoCode = typeof CryptoCodes[number];  // same as union 'BTC'|'ETH'|'LTC'|'DOGE'

see Also


2 comments: