TypeScript Expression Magic

TypeScript is a truly beautiful language. In its arsenal there is everything that is necessary for high-quality development. And if suddenly, someone is familiar with sex-dramatic sketches with JavaScript, then I will understand. TypeScript has a number of assumptions, unexpected syntax, and delightful constructions that emphasize its beauty, shape and fill with new meaning. Today we are talking about them, about these assumptions, about the magic of expressions. Who cares, welcome.



A bit of truth



True N1.



Most of the designs and unexpected finds outlined below, I first caught my eye on the pages of Stack Overflow, github or were invented by myself. And only then it dawned on - all this is here or here . Therefore, I ask you to treat with understanding in advance if the stated findings seem trite to you.



True N2.



The practical value of some designs is 0.



True N3.



Examples were tested under tsc version 3.4.5 and target es5. Just in case, under the spoiler config



tsconfig.json
{

"CompilerOptions": {

"OutFile": "./target/result.js",

"Module": "amd",

"Target": "es5",

"Declaration": true,

"NoImplicitAny": true,

"NoImplicitReturns": true,

"StrictNullChecks": true,

"StrictPropertyInitialization": true,

"ExperimentalDecorators": true,

"EmitDecoratorMetadata": true,

"PreserveConstEnums": true,

"NoResolve": true,

"SourceMap": true,

"InlineSources": true

},

"Include": [

"./src"

]

}





Implementation and Inheritance



Find : in the implements section you can specify interfaces, types and classes . We are interested in the latter. Details here



abstract class ClassA { abstract getA(): string; } abstract class ClassB { abstract getB(): string; } // , tsc     abstract class ClassC implements ClassA, ClassB { // ^  ,   implements  . abstract getA(): string; abstract getB(): string; }
      
      





I think TypeScript developers took care of the 'strict contracts' executed through the class keyword. Moreover, classes do not have to be abstract.



Find : expressions are allowed in the extends section. Details If to ask a question - whether it is possible to inherit from 2 classes, then the formal answer is no. But if you mean exporting functionality - yes.



 class One { one = "__one__"; getOne(): string { return "one"; } } class Two { two = "__two__"; getTwo(): string { return "two"; } } //  ,    :   IDE (  )    . class BothTogether extends mix(One, Two) { // ^   ,    extends   info(): string { return "BothTogether: " + this.getOne() + " and " + this.getTwo() + ", one: " + this.one + ", two: " + this.two; // ^   IDE   ^  } } type FaceType<T> = { [K in keyof T]: T[K]; }; type Constructor<T> = { // prototype: T & {[key: string]: any}; new(): T; }; // TODO:    ,   .       function mix<O, T, Mix = O & T>(o: Constructor<O>, t: Constructor<T>): FaceType<Mix> & Constructor<Mix> { function MixinClass(...args: any) { o.apply(this, args); t.apply(this, args); } const ignoreNamesFilter = (name: string) => ["constructor"].indexOf(name) === -1; [o, t].forEach(baseCtor => { Object.getOwnPropertyNames(baseCtor.prototype).filter(ignoreNamesFilter).forEach(name => { MixinClass.prototype[name] = baseCtor.prototype[name]; }); }); return MixinClass as any; } const bt = new BothTogether(); window.console.log(bt.info()); // >> BothTogether: one and two, one: __one__, two: __two__
      
      





Find : a deep and at the same time meaningless anonym.



 const lass = class extends class extends class extends class extends class {} {} {} {} {};
      
      





And who will write the word class with 4 extends in the example above?



If so
 // tslint:disable const Class = class Class extends class Class extends class Class extends class Class extends class Class {} {} {} {} {};
      
      





And more?



Like this
 // tslint:disable const lass = class Class<Class> extends class Class extends class Class extends class Class extends class Class {} {} {} {} {};
      
      





Well, you understand - just a class!



Exclamation Mark - Unlimited Operator and Modifier





If you do not use strictNullChecks and strictPropertyInitialization compilation settings,

then most likely the knowledge about the exclamation mark passed near you ... In addition to the main purpose, 2 more roles are assigned to it.



Find : Exclamation mark as Non-null assertion operator



This operator allows you to access the structure field, which can be null without checking for null. An example with an explanation:



  //     --strictNullChecks type OptType = { maybe?: { data: string; }; }; // ... function process(optType: OptType) { completeOptFields(optType); //   ,   completeOptFields    . window.console.log(optType.maybe!.data); // ^ -    ,    null //   !,    tsc: Object is possibly 'undefined' } function completeOptFields(optType: OptType) { if (!optType.maybe) { optType.maybe = { data: "some default info" }; } }
      
      





In total, this operator allows you to remove unnecessary checks for null in the code, if we are sure ...



Find : Exclamation mark as Definite assignment assertion modifier



This modifier will allow us to initialize the class property later, somewhere in the code, with the strictPropertyInitialization compilation option turned on. An example with an explanation:



 //     --strictPropertyInitialization class Field { foo!: number; // ^ // Notice this '!' modifier. // This is the "definite assignment assertion" constructor() { this.initialize(); } initialize() { this.foo = 0; // ^   } }
      
      





But all this mini-calculation about the exclamation point would not make sense without a moment of humor.



Question: Do you think the following expression will compile?



 //     --strictNullChecks type OptType = { maybe?: { data: string; }; }; function process(optType: OptType) { if (!!!optType.maybe!!!) { window.console.log("Just for fun"); } window.console.log(optType.maybe!!!!.data); }
      
      





Answer
Yes


Types





Everyone who writes complex types discovers a lot of interesting things. So I got lucky.



Find : a subtype can be referenced by the name of a field of the main type.



 type Person = { id: string; name: string; address: { city: string; street: string; house: string; } }; type Address = Person["address"];
      
      





When you write types yourself, this declaration approach hardly makes sense. But it happens that a type comes from an external library, but a subtype does not.



A subtype trick can also be used to improve code readability. Imagine that you have a base class with a generic type that the classes inherit from. The example below illustrates what has been said.



 class BaseDialog<In, Out> { show(params: In): Out {/**  .   return ... */ } } //  - class PersonDialogOld extends BaseDialog<Person[], string> {/**   */} //   class PersonDialog extends BaseDialog<Person[], Person["id"]> {/**   */}
      
      





Find : with the help of the TypeScript type system it is possible to achieve a combinatorial set of generated types with coverage of the desired functionality. Difficult to say, I know. I thought about this formulation for a long time. I will show you an example of the Builder template, as one of the most famous. Imagine that you need to build an object using this design pattern.



 class SimpleBuilder { private constructor() {} static create(): SimpleBuilder { return new SimpleBuilder(); } firstName(firstName: string): this { return this; } lastName(lastName: string): this { return this; } middleName(midleName: string): this { return this; } build(): string { return "what you needs"; } } const builder = SimpleBuilder.create(); //     . const result = builder.firstName("F").lastName("L").middleName("M").build();
      
      





For now, don’t look at the redundant create method, the private constructor, and generally the use of this template in ts. Focus on the call chain. The idea is that the called methods should be used strictly 1 time. And your IDE should also be aware of this. In other words, after calling any method on the builder instance, this method should be excluded from the list of available ones. To achieve such functionality, the NarrowCallside type will help us.



 type ExcludeMethod<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; type NarrowCallside<T> = { [P in keyof T]: T[P] extends (...args: any) => T ? ReturnType<T[P]> extends T ? (...args: Parameters<T[P]>) => NarrowCallside<ExcludeMethod<T, P>> : T[P] : T[P]; }; class SimpleBuilder { private constructor() {} static create(): NarrowCallside<SimpleBuilder> { return new SimpleBuilder(); } firstName(firstName: string): this { return this; } lastName(lastName: string): this { return this; } middleName(midleName: string): this { return this; } build(): string { return "what you needs"; } } const builder = SimpleBuilder.create(); const result = builder.firstName("F") // ^ -    .lastName("L") // ^ -   lastName, middleName  build .middleName("M") // ^ -   middleName  build .build(); // ^ -    build
      
      





Find : Using the TypeScript type system, you can control the sequence of calls by specifying a strict order. In the example below, using the DirectCallside type, we demonstrate this.



 type FilterKeys<T> = ({[P in keyof T]: T[P] extends (...args: any) => any ? ReturnType<T[P]> extends never ? never : P : never })[keyof T]; type FilterMethods<T> = Pick<T, FilterKeys<T>>; type BaseDirectCallside<T, Direct extends any[]> = FilterMethods<{ [Key in keyof T]: T[Key] extends ((...args: any) => T) ? ((..._: Direct) => any) extends ((_: infer First, ..._1: infer Next) => any) ? First extends Key ? (...args: Parameters<T[Key]>) => BaseDirectCallside<T, Next> : never : never : T[Key] }>; type DirectCallside<T, P extends Array<keyof T>> = BaseDirectCallside<T, P>; class StrongBuilder { private constructor() {} static create(): DirectCallside<StrongBuilder, ["firstName", "lastName", "middleName"]> { return new StrongBuilder() as any; } firstName(firstName: string): this { return this; } lastName(lastName: string): this { return this; } middleName(midleName: string): this { return this; } build(): string { return "what you needs"; } } const sBuilder = StrongBuilder.create(); const sResult = sBuilder.firstName("F") // ^ -   firstName  build .lastName("L") // ^ -   lastName  build .middleName("M") // ^ -   middleName  build .build(); // ^ -   build
      
      





Total





These are all my interesting findings on TypeScript today. Thank you all for your attention and see you soon.



All Articles