Typescript: Accumulating type in TypeScript like powerful instrument for organizing code

Created on 5 Feb 2020  ·  4Comments  ·  Source: microsoft/TypeScript

Main Idea

Accumulating type is a type that combines the types of declarations that occur in different parts of the code with the same name. Type merging is done according to the OR principle.
Principles and dogmas:

  • related code in one place
  • organizing separate and independent modules
  • write once use everywhere
  • intermodular interaction through types

Search Terms

accumulate type, type system

Suggestion

Add the ability to declare accumulative types

Examples

// file: moduleAFolder/moduleA.ts
    accumulate type ModuleNames = "ModuleA"; // type to be "ModuleA" | "ModuleB"

// file: moduleAFolder/moduleAHelper.ts
    import {ModuleNames} from "moduleAFolder/moduleA";
    //      ^ powerful part of example: importing type ONLY from moduleA, NOT from ModuleB
    //      ^ accessible value for ModuleNames here are "ModuleA" and "ModuleB"

    const a: ModuleNames = "ModuleA"; // it's ok
    const b: ModuleNames = "ModuleB"; // it's ok.
    const c: ModuleNames = "Unknown"; // it's error

// file: moduleBFolder/moduleB.ts
    accumulate type ModuleNames = "ModuleB"; // type to be "ModuleA" | "ModuleB";                   

Example above is very simple but illustrates all principles good code. In more difficalt cases we have 100 modules and 1000 types. Some of them has types that need in another modules.

Yes, we can create folder with name 'shared' and move common types to this folder:

  • but then we destroy the integrity of the code inside the separate module. Everything that looked whole before that became fragmented;
  • but then we make the “shared” folder a trash or garbage;

Yes, we can copy common types from one module to another:

  • but then we make smelling bad code;
  • but then we make bug prone code;

More difficult example

 // file: moduleAFolder/moduleA.ts
     accumulate type CrossModuleTypes = {
         "ModuleA": {
             id: string;
             data: {
                 value: string;
             }
         }
     }


 // file: moduleBFolder/moduleB.ts
     accumulate type CrossModuleTypes = {
         "ModuleB": {
             id: string;
             segment: {
                 part: string;
             }
         }
     }

 // file: moduleCFolder/moduleC.ts
     // no imports here!
     accumulate type CrossModuleTypes; // nothing merge, only usaging;
     type UnionToIntersection<U> =  (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never
     type ModuleBType = UnionToIntersection<CrossModuleTypes>["ModuleB"];
     const cachingData: ModuleBType = this.loadFromCache("ModuleB");
     console.log("Caching segment from ModuleB is ", cachingData.segment.part); // no errors here.

Checklist

My suggestion meets these guidelines:

  • [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • [x] This wouldn't change the runtime behavior of existing JavaScript code
  • [x] This could be implemented without emitting different JS based on the types of the expressions
  • [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • [x] This feature would agree with the rest of TypeScript's Design Goals.
In Discussion Suggestion

Most helpful comment

You can do this with already with interface merging, so long as you're OK with making up a unique fake property name for each union constituent:

interface Foo {
   whatever: "moduleA";
}

interface Foo {
   whateverwhatever: "moduleB";
}

type AccumulatedFoo = Foo[keyof Foo]; // "moduleA" | "moduleB"

powerful part of example: importing type ONLY from moduleA, NOT from ModuleB

This part sounds difficult. It means an accumulate type has an effectively global name, even if it's a module scoped type, because any other module anywhere could declare an accumulate type with the same name and they would merge.

All 4 comments

I need this feature too

You can do this with already with interface merging, so long as you're OK with making up a unique fake property name for each union constituent:

interface Foo {
   whatever: "moduleA";
}

interface Foo {
   whateverwhatever: "moduleB";
}

type AccumulatedFoo = Foo[keyof Foo]; // "moduleA" | "moduleB"

powerful part of example: importing type ONLY from moduleA, NOT from ModuleB

This part sounds difficult. It means an accumulate type has an effectively global name, even if it's a module scoped type, because any other module anywhere could declare an accumulate type with the same name and they would merge.

Yes, 'accumulate type' in a sense, it can be read as 'global type'. This is the main point of the idea: merge types across root project.

You can do this with already with interface merging. Yes i can, but where Foo interface were placed?

Yes i can, but where Foo interface were placed?

I don't know all of the exact rules, but you're certainly allowed to have an interface merge across many different files.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Antony-Jones picture Antony-Jones  ·  3Comments

DanielRosenwasser picture DanielRosenwasser  ·  3Comments

MartynasZilinskas picture MartynasZilinskas  ·  3Comments

seanzer picture seanzer  ·  3Comments

bgrieder picture bgrieder  ·  3Comments