Working on a suite of applications for a big enterprise, one understands the benefits of reusable components quite quickly. Chances are, at least 50% of functionality, like a user-friendly customer lookup, will be used in the majority of your applications with just a little bit variation in its functionality. Your users expect that this widget will be in a familiar place in every app, will look roughly the same and will act exacly the same. As an experienced DEV, you take a base component library and build your CustomerSelectorCmp on top of it up to the current requirements. It has all the necessary logic, it is used in 20 apps and it works great. Until new requirements come:
- A new app is being commissioned, and CustomerSelectorCmp needs to have a horizontal mode for it; This will probably be used only in this 1 app;
- Our managers have doubled sales numbers by grabbing tablets for client visits and having our app ready to go in the heat of the moment; But CustomerSelectorCmp could be much better with touch;
- We have 10% of managers with special visual needs – it would be very nice to have accessibility mode for CustomerSelectorCmp;
5 years ago, trying to accommodate all this functionality in a single component would inevitably have to violate Single Responsibility Principle and lead to an unmaintainable monstrosity, where key CSS properties are overridden at-least 3 times each, classes are added just to increase CSS selector specificity and !important is your best and worst friend. Well, lucky we are in 2020! Thanks to CSS-Grid and React, we can have a base component that contains all the key functionality and can change appearance drastically with just a dozen lines of CSS and JS. See the following 3 dropdowns? They are the same component, rendering the same DOM! The difference is in 6-8 lines of CSS!
Grab the showcase project form github and lets dive in.
This article will help you with debugging modern Web aps. ‘Modern’ here specifically means apps where source code undergoes a major transformation before execution, e.g. you are using Babel to enable features from ES2015-ES2018, TypeScript, and/or frameworks like Mobx that rely on transformations like decorators and transpile down to ES5. Because of the above, code that is actually executed by JS runtime is very different from what you see in your IDE.
You may be thinking, ‘but we’ve got source maps !’ While source maps are good, transforming ES2015-ES2018 features into ES5 changes code too dramatically, a single source line often becomes 3 or more during execution, this becomes _this, variable names change due to transpilation of let and const into var, etc. All of these changes really mess with debugging features like step over or examining variable values by their name. Debugging it can be a pain, but it does not have to.
Sample project for this article is avaliable on GithubGeneric classes in C# and Java can be thought of as class templates, which let us use placeholders instead of certain types. What they don't allow is having generic property names. Typescript does not have such limitation and easily lets us do that:
export type MakeProp<TKey extends string , TType> = {
[key in TKey]: TType
}
// MakeProp<'foo', number>
type equivalent1 = {
foo: number;
}
TS playground ↗
This does not look so impressive, after all, we could have written this type by hand just as easily. What we should know, however, is that we have passed they type-key barrier ( not a real term :-) ) - we managed to make type into a key and create an object shape using only other types. This means, we can use our 'MakeProp' type within other generic types in Typescript. With mapped and conditional types, this means we can construct much more complex types with our mappings.
If you keep track of TypeScript innovations, you've probably used a Mapped Type like Partial, and maybe even pressed “Go to definition” to see, how it is defined under the hood.
type Partial<T> = {
[P in keyof T]?: T[P];
}
Sadly, while many people are using predefined Mapped Types, few have adapted writing their own to utilize the full power of Typescript. Even fewer still realize the full extent of their power in types like the following:
// Readonly that is applied recursively
// to properties which are objects, instead of just first level
type ReadonlyDeep<TType> = {
readonly [key in keyof TType]: TType extends Object
? ReadonlyDeep<TType[key]>
: TType[key];
}
let t = {b: 1, c: {d:2}}
let readonly: Readonly<typeof t> = t;
readonly.c.d = 5; // :-( no error
let readonlyDeep: ReadonlyDeep<typeof t> = t;
readonlyDeep.c.d = 5; // :-) error!
// A way to get keoyf only for certain types of keys
type KeyofMethods<TType> = ({
[key in keyof TType]: TType[key] extends Function
? key // notice, use of key instead of TType[key]
: never
})[keyof TType];
class FooBar {
a: number = 0;
c(){ return 0;};
d(){ return 0;};
}
type km = KeyofMethods<FooBar>; //"c"|"d"
// A way to prohibit access to anything that is not a function,
// to enforce method usage in part of code, while having relaxed rules elsewhere
type MethodsOf<TType> = Pick<TType, KeyofMethods<TType>>
const m = <MethodsOf<FooBar>> new FooBar();
let c: number = m.c(); // fine
let a = m.a; //error!
TS playground ↗
Lets explore the practical application and peek into still more fantastic possibilities.
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
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!
And that is just the tip of the iceberg, real power comes in type!