Using private properties of a class to strengthen typing in typescript

This is what I love about typescript because it keeps me from spanking nonsense. Measure the length of a numeric value, etc. At first, of course, I spat, indignant that I was being pestered with all sorts of stupid formalities. But then I got involved, fell in love harder. Well, in the sense of a little bit more strict. Included in the project the strictNullChecks option and spent three days fixing the errors that occurred. And then he rejoiced with satisfaction, noting how easy and natural the refactoring is now.





But then you want something even more. And here the typescript needs to explain what restrictions you impose on yourself, and you delegate to him the responsibility to monitor compliance with these restrictions. Come on, break me completely.





Example 1

Some time ago, I was captured by the idea of ​​using react as a templating engine on the server. Captured of course by the possibility of typing. Yes, there are all sorts of pug, mustache and what else there. But the developer has to keep in mind himself whether he forgot to expand the argument passed to the template with new fields. (If this is not the case, correct me. But in general, I do not care - thank God I do not have to deal with the generation of templates by the nature of my work. And an example about something else).





And here we can normally type the props passed to the component and get the corresponding IDE hints when editing the template. But this is inside the component. Now let's make sure that we have not transferred any leftism to this component.





import { createElement, FunctionComponent, ComponentClass } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';

export class Rendered<P> extends String {
  constructor(component: FunctionComponent<P> | ComponentClass<P>, props: P) {
    super('<!DOCTYPE html>' + renderToStaticMarkup(
      createElement(component, props),
    ));
  }
}
      
      



Now, if we try to transfer props from the order to the user component, we will immediately be alerted to this misunderstanding. Cool? Cool.





But this is at the time of html generation. How are things going with its further use? Because the result of instantiating Rendered is just a string, then typescript will not swear, for example, with the following construction:





const html: Rendered<SomeProps> = 'Typescript cannot into space';
      
      



Accordingly, if we write something like this:





@Get()
public index(): Rendered<IHelloWorld> {
  return new Rendered(HelloWorldComponent, helloWorldProps);
}
      
      



this does not in any way guarantee that the compilation result of the HelloWorldComponent will be returned from this method .





, :)





export class Rendered<P> extends String {
	_props: P;
	constructor(component: FunctionComponent<P> | ComponentClass<P>, props: P)
...
      
      



'cannot into space' , _props. . - . - _props, js , .. "" .









Object.assign('cannot into space', {_props: 42})
      
      



, . .





export class Rendered<P> extends String {
  // @ts-ignore -       noUnusedParameters
  private readonly _props: P;
  constructor(component: FunctionComponent<P> | ComponentClass<P>, props: P)
...
      
      



Object.assign , .. Rendered



_props , .





, , , . , , . .





2

, , , . - -. . .





. , . .





, -, .





ApiResponse. - , .





export interface IApiResponse {
	readonly scenarioSuccess: boolean;
	readonly systemSuccess: boolean;
	readonly result: string | null;
	readonly error: string | null;
	readonly payload: string | null;
}

export class ApiResponse implements IApiResponse {
	constructor(
		public readonly scenarioSuccess: boolean,
		public readonly systemSuccess: boolean,
		public readonly result: string | null = null,
		public readonly error: string | null = null,
		public readonly payload: string | null = null,
	) {}
}
      
      



scenarioSuccess true. , ( ) - scenarioSuccess false. - systemSuccess false. / result/error. . , scenarioSuccess true error.





, ApiResponse , :





export class ScenarioSuccessResponse extends ApiResponse {
  constructor(result: string, payload: string | null = null) {
    super(true, true, result, null, payload);
  }
}
      
      



.





- ApiResponse, " " , . .





const SECRET_SYMBOL = Symbol('SECRET_SYMBOL');

export abstract class ApiResponse implements IApiResponse {
  // @ts-ignore
  private readonly [SECRET_SYMBOL]: unknown;
  
  constructor(
    public readonly scenarioSuccess: boolean,
    public readonly systemSuccess:   boolean,
    public readonly result:  string | null = null,
    public readonly error:   string | null = null,
    public readonly payload: string | null = null,
  ) {}
}
      
      



Rendered



_props, , , . "" . . ( , ?)





. , , any. .





Here you can also notice that communication between the components of the system must be decoupled through interfaces. But it is quite possible for the receiving side to prescribe that it expects an IApiResponse , but the service from the domain logic layer is so be it, let it return a specific implementation of ApiResponse .





Well ...

I hope this material was interesting for you. To some, this approach may seem redundant, and I do not urge everyone to urgently add such "guards" to their projects. But I hope you found food for thought in my article.





Thank you for your time. I would be glad to receive constructive criticism.








All Articles