Recent articles About

Compiling enterprise

Ivan Koshelev blog on software development

Pragmatic uses of Typescript type system 01 My type of type [2019 February 22] Typescript, types, interfaces, duck typing

You may have heard, that TypeScripts type system has been prooven turing-complete . In layman’s terms this means you can write type definitions in such a way, that compiler will execute a simple program from just trying to compile them. A fascinating fact, but how is it useful in our daily work? It tells us, that TS type system is on a whole next level compared to what we are used to in Java and C#, and we should rethink our approach to it. In this article, we won't delve into the arcana of things like conditional types, but will concentrate on the often overlooked (especially by devs coming from the static typing world) practical uses of TS type system, that make daily life of a dev much easier.

Robert C. Martin of Clean Code fame once said, it would be nice if we could instruct compiler to use concrete types in Java only for their signature of abstractions, i.e., interfaces. Indeed, for 95% of enterprise code I work with, formal interface types serve as a technical detail, needed by the compiler only. Such case is easy to recognize: your interface has only 1 implementation (not counting testing mocks) and you don't have to ship/deploy this implementation separately (it is in the same DLL as interface, or the dll is separate, but always goes together). This is a clear sign:

  • this interface is driven by a single use case, which is fully reflected in single concrete implementation
  • only needed to teach compiler about proper dependency abstraction
  • you as a dev don't get anything out of it, worse yet, you are forced to duplicate member definitions
Ask yourself, how many interfaces in your application can be resolved by your IoC container automatically? Realizing this, I always wanted to be able to decorate my C# classes with a keyword or attribute, that would give me an automatic interface based on their concrete implementation and containing all their public members. Well, guess what we can do in TS?

class Foo {
    bar: number;
    baz() { console.log(this.bar); }
}

// An interface extends a class!
interface IFoo extends Foo { }

let foo = {} as IFoo;

let bar:number = foo.bar;
foo.baz();                  //type checks ok!

TS playground

And that is just the tip of the iceberg, real power comes in type!

Extracting type and nested-type information out of variables

Typescript does a great job of inferring type information for you and performing safety checks.

let inst = {
    a: 1,
    b: 2,
    subInst: {
        c: 3,
        d: 4
    }
}

console.log(inst.a); // fine!
console.log(inst.e); // error!
console.log(inst.subInst.c); // fine!
console.log(inst.subInst.f); // error!

TS playground

And you probably know that 'typeof' operator allows you to extract type information from an existing variable, for example, if you vant another variable compatible with the first.

let inst = {
    a: 1,
    b: 2,
    subInst: {
        c: 3,
        d: 4
    }
}

let inst2: typeof inst;

inst2 = inst;   // type check ok!
console.log(inst2.a); // fine!
console.log(inst2.e); // error!
console.log(inst2.subInst.c); // fine!
console.log(inst2.subInst.f); // error!

TS playground

But what happens if you need to create a new variable with the same type as 'subInst' property? Can we tell TS 'this variable should have a type to make it compatible with that property'? Yes, we can.

let inst = {
    a: 1,
    b: 2,
    subInst: {
        c: 3,
        d: 4
    }
}

let anotherSubInst: (typeof inst)['subInst'] = {
  c: 5, // fine
  d: 6, // fine
  e: 7  // error! type does not have such prop 
}

console.log(anotherSubInst.c); // type check fine

TS playground

Above code demonstrates, that you can think of types in TS as JS objects containing type information.

This example embodies a shift in our view of the type system. Often we don't need to introduce new entities needed solely to describe shape of our types, we can define their shape in the most direct and succinct way possible and then tell TS to extract a type definition out of them.

If you are using React with TS, you may be using interfaces or types to describe props and state of your components. This adds extra named entities to your code. What is the chance you will need to explicitly reuse the type of your components props in full? And for state this has no sense at all: interfaces by definition describe a public signature of a type, but state of React component is by definition private. Just describe the shape of the props and state as you go and don't worry about reuse - if you ever need to extract type or parts of type from it, you can always do it.


class SampleCmp extends React.Component<{ 
        foo: number
    },{
        bar: number
    }>{

    render(){<></>}
}

type PropsOf<TCmp extends React.Component> = TCmp['props'];

let props:  PropsOf<SampleCmp> = {
    foo: 5  //type check ok!
}

What if you wanted to get an element type out of an array? There are, in fact, several ways.

let arr: number[];

// easy way
let elemtInst: (typeof arr)[0];
elemtInst = 5;  // type check ok

// more technical way using 'infer' keyword and conditional types
// (more on this later)
type ElementType1<TArr extends any[]> =
                        TArr extends (infer T)[] ? T : any;

let elemInst1: ElementType1<typeof arr>;
elemInst1 = 7;   // type check ok

// equivalent but more verbose example using 'infer'
type ElementType2<TArr extends Array<any>> =
                        TArr extends Array<(infer T)> ? T : any;

let elemInst2: ElementType2<typeof arr>;
elemtInst = 11;  // type check ok

TS playground

What is the difference between this two ways? For one, TS has tuples based on JS arrays, and for them elements at different index will have different types.

let tuple: [number,string];

let elemtInst: (typeof tuple)[0]; //number
elemtInst = 5;  // type check ok
elemtInst = "";  // error!

type ElementType<TArr extends any[]> =
                    TArr extends (infer T)[] ? T : any;

let elemInst1: ElementType<typeof tuple>; //number|string
elemInst1 = 7;   // type check ok
elemInst1 = "";  // type check ok
elemInst1 = true; // error!

TS playground

Using type like an interface

You have probably heard of very useful TS types like Partial<T>, that let you produce a type derived from T, but with every property being optional.

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

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

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

As you can see, each of those types adds or removes a modifier like 'readonly' to each property. But what would happen if you wrote this signature without specifying any modification?

type PublicInterfaceOf<TType> = {
    [TProp in keyof TType]: TType[TProp]
}

From the name of this type, you have already guessed what will happen - you get a public interface just like the case we started with.

class Foo {
    bar: number;
    baz() { console.log(this.bar); }
}

type PublicInterfaceOf<TType> = {
    [TProp in keyof TType]: TType[TProp]
}

let foo = {} as PublicInterfaceOf<Foo>;

let bar:number = foo.bar;
foo.baz();                  //type checks ok!

TS playground

What if you wanted to export just a subset of members from a type? Pick<> type from stadard library will help you.

class Foo {
    bar: number;
    baz() { console.log(this.bar); };
    bac: string;
}

type PickSomeFromFoo = Pick<Foo, 'bar' | 'bac'>;

let foo2 = {} as PickSomeFromFoo;
let bar2:number = foo2.bar;
foo2.baz();                  //error!

// As you probably guessed, broadest possbile Pick
// picks all keys and, as such, is equivalent to PublicInterfaceOf
type PickAllFromFoo =  Pick<Foo, keyof Foo>;

let foo = {} as PickAllFromFoo;
let bar:number = foo.bar;
foo.baz();                  //type checks ok!

TS playground

Extracting type information from function types

Ok. This are all object. But what about functions? What if someone has described a shape of function parameter inline? No worries, the standard library of TS itself has got us covered here.

function foo(bar: string, baz: { a: number, b: string }) {
  //...
}

type Baz = Parameters<typeof foo>[1];

let baz: Baz = {
  a: 1,
  b: '' //type checks ok!
}

TS playground

Actually, standard library provides you with a plethora of types meant to extract type information out of function signatures via the 'infer' keyword.

/**
 * Obtain the parameters of a function type in a tuple
 */
type Parameters<T extends (...args: any[]) => any> = T extends (...args: infer P) => any ? P : never;

/**
 * Obtain the parameters of a constructor function type in a tuple
 */
type ConstructorParameters<T extends new (...args: any[]) => any> = T extends new (...args: infer P) => any ? P : never;

/**
 * Obtain the return type of a function type
 */
type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : any;

/**
 * Obtain the return type of a constructor function type
 */
type InstanceType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer R ? R : any;

Above types are especially useful, since quite often library definitons export functions but don't export their parameter or return types. This is how you can extract those types manually and easily.

Combining all the techniques

Combining different types, you can get to the shape of any type that is at-least somewhat publicly available.

function foo() {
  return {
    a: 1,
    b: 2,
    subInstArr: [{
        c: 3,
        d: 4
    }]
  }
}

type InstType = ReturnType<typeof foo>
type SubInstArr = InstType['subInstArr'];
type SubIsntType = SubInstArr[0];

let baz: SubIsntType = {
  c: 5,
  d: 6 // type checks ok!
}

//You could just write a one-liner,
//But please make sure it is forward-readable
//(you can understand it from reading once left-to-right with no jumps)
type SubIsntType2 = ReturnType<typeof foo>['subInstArr'][0];
let baz2: SubIsntType2 = {
  c: 5,
  d: 6 // type checks ok!
}

TS playground

Hopefully, this techniques will encourage you to write TypeScript just like you would JavaScript or other dynamic languages - shaping types as you go instead of upfront, letting the compiler infer type information to keep you safe, and letting other devs reuse it when THEY need it without roadblocks.


Ivan Koshelev photo

I'm passionate for anything that can be programed and automated to make life better for all of us.

Archives

  1. January 2023 (1)
  2. January 2022 (2)
  3. November 2021 (1)
  4. May 2021 (1)
  5. March 2020 (1)
  6. August 2019 (1)
  7. July 2019 (1)
  8. May 2019 (1)
  9. February 2019 (1)
  10. October 2017 (1)
  11. August 2017 (3)
  12. July 2017 (3)
  13. May 2017 (3)

Elsewhere

  1. GitHub@IKoshelev
  2. LinkedIn
  3. NuGet@IKoshelev
  4. NPM@IKoshelev