Typescript keeping it stupidly simple
Typescript is a superset of Javascript and brings a lot of advantages for javascript development by offering us a way to check errors statically, without having to run or execute or transpile the code. We also get a better understanding of our code base by having it documented through our type annotations; this opens up features such as code completion as it provides a linkage between our system entities and allows us to feel more comfortable when refactoring, or finding an implementation interface for any declaration directly in our code editor, for example.
Junior vs Mid vs Senior Engineer
Myself, like to use Typescript as it pleases me, which means simplicity wherever possible. The expressiveness of javascript is a must for the work I tend to develop and the goals and principles I like to fulfill.
Typescript does not replace javascript and I don’t look at it as a language - of course it has some language constructs or inventions of its own (e.g. enums, function overloads, etc) but you wouldn’t write Typescript without knowingly understand the underlying system.
Tips and tricks#
Bellow I mention a few tips and tricks that I find more readable when writting Typescript.
Type annotation for primitives can be kept in the function declaration. A meaningful name for the function is enough and much preferred against the fn parameter descriptor.
Primitives#
No!
type TName = string
type TUpperName = string
/**
* @param {name} string - The name to convert to uppercase
* @returns {string} `name`, transformed to uppercase
*/
function transform(name: TName): TUpperName {
return name.toUpperCase()
}
Yes!
function toUppercase(name: string): string {
return name.toUpperCase()
}
I’d like to leave a note here and say that Interface
is the preferred method to create type annotations in TypeScript,
there are differences but here we noticed that Type
works with primites while Interface
would not.
Would it be correct to accept type inferrence in this case? Remove the annotation’s completely? We could but we’d miss out in the documentation we just took out because we’re relying on our type annotation!
Make sure you understand the Typescript syntax that gives you immediate summaries of how the code works without having to read long descriptions that don’t keep up with the code and required manual update!
There might be cases where “trust me, I know what I’m doing” come useful and this is what is called type casting, but it won’t do any checks for you and will assume that you, the programmer, has put any guards or other procedures necessary.
Why? When “trust me, I know what I’m doing”, ask yourself why you’re doing it.
function toUppercase(name) {
// return (<string>name).toUpperCase()
return (name as string).toUpperCase()
}
No!
const toUppercase: (name: any) => string = (num: number) => String(num)
Ok!
const toUppercase = (num: number) => String(num)
Yes!
const toUppercase: (name: any) => string = (num: number) => num
The type annotation for toUppercase
declared return type is used and Typescript infers the result return type for the function.
Just stick with the tip “type down the return type that is expected, it doesn’t do any harm!”.
Project architecture#
No!
/root
/dist
/src
/ts
app.ts
/js
app.js
tsconfig.json
Yes!
/root
/dist
/src
/ts
app.ts
tsconfig.json
Unless you’re distributing deliverables or importables (consumed in other applications as modules or units), I personally do not find any benefits in transpiling to an adjacent Javascript source directory.
Assuming you have control over the application infrastructure and direction, if the project is written in Typescript keep it that way!
You are not going to need to edit the javascript source files. If introducing Typescript in a legacy project, what you can do instead is
to have Typescript coexist with Javascript by using the options available through the Typescript transpiler (https://www.typescriptlang.org/docs/handbook/compiler-options.html). Some of these are: --allowJs
, --checkJs
, etc.
For many years used Babel to orchestrate my Javascript and eventually my Typescript transpilation, even when using Typescript solo, @babel/preset-typescript (https://babeljs.io/docs/en/babel-preset-typescript.html).
Babel is the defacto transpiler for Javascript since the early days and I use it for its simplicity! It’s also faster tham the Typescript transpiler, because it simply removes Typescript ahead of the transpilation process and works on the javascript source-code.
To complete, as in javascript do not checkin the dist
directory onto your repository or any transpiled source code for the same reason stated
above, unless you are interoperating with javascript in a legacy project; Keep your sources the source of truth! Don’t expect any hocus-pocus to
generate human readable javascript from Typescript, even if the vendor says so.
External libraries#
When working with external libraries that are originally written and delivered in Javascript, even when @Types are available to you,
consider the option --noImplicitAny
; I’ve experienced cases where Type annotation libraries such as (https://cesium.com/) take a
considerable amount of time that in any way is productive when solving the main business logic. Be wise and use Typescript whenever
possible but that does not mean being counter productive, trying to decipher libraries that have been maintained for 10 years and require
a high degree of understanding on subjects such as cartography, etc. Be humble and acceptant but cautious! Nurture the underlying target
of Typescript, it’s dynamic level that is javascript that consists of execution of source-code at runtime, we’ve been doing that for years
and been very successful! Do not fear it and and do not waste your sanity forcing Typescript annotations into everything!
Keep this in mind:
Values exist at the dynamic level! Types exist at the static level!
Either this do X, that do Z#
Union types sound wonderful and more often then not, you come across a situation where you use this type annotation. Or even better,
you write a function that do some computations under certain clauses. If A
you do foo
and when B
you do bar
! My advice here is
that whenever possible give a single responsability to a function! Move computations as reusable helpers and use function overloads
(https://www.typescriptlang.org/docs/handbook/functions.html#overloads) and even better use Generics
.
No!
interface IPerson {
fullName: string,
phoneNumber: number,
email: string
}
function findPerson(personalDetail: string | number | IFoobar | IBarfoo | ILorem | IIpsum): IPerson {
if (typeof personalDetail === 'string') {
// find in list of Person string properties
} else if (typeof personalDetail === 'number') {
// find in list of Person number properties
} else {
throw new Error("Person not found!")
}
}
Ok!
function findPerson(personalDetail: string): IPerson;
function findPerson(personalDetail: number): IPerson;
function findPerson(personalDetail: IFoobar): IPerson;
function findPerson(personalDetail: IBarfoo): IPerson;
function findPerson(personalDetail: ILorem): IPerson;
function findPerson(personalDetail: IIpsum): IPerson;
function findPerson(personalDetail): IPerson {
if (typeof personalDetail === 'string') {
// find in list of Person string properties
} else if (typeof personalDetail === 'number') {
// find in list of Person number properties
} else {
throw new Error("Person not found!")
}
}
Yes!
function findPerson<T>(personalDetail: T): IPerson {
if (typeof personalDetail === 'string') {
// find in list of Person string properties
} else if (typeof personalDetail === 'number') {
// find in list of Person number properties
} else {
throw new Error("Person not found!")
}
}
As stated above, values exist dynamically and types exist statically, which means:
Dynamic!
const factory = (y: number) => y
const value = factory(300)
Static!
type TBool = boolean
type TFactory<T> = T
type TType = TFactory<TBool>
Now, of course that you might be tempted to use the Generic T to evaluate it with typeof
.
function findPerson<T>(personalDetail: T): IPerson {
if (typeof personalDetail === T) {
// find in list of Person string properties
// find in list of Person number properties
} else {
throw new Error("Person not found!")
}
}
This does not work because the code you are writting is only used statically, after transpiled the type of T
is removed; As such there
is not T value
, so nothing there to compute against in the clause.
You of course might still want to use it but I think the process introduces unreadable code (https://github.com/Microsoft/TypeScript/wiki/FAQ#why-cant-i-write-typeof-t-new-t-or-instanceof-t-in-my-generic-function) and goes against keeping things simple.
When deciding on this things, stick with the Marie Kondo principle when writting Typescript “does this code spark joy?”
Describing Interfaces#
No!
interface IPoint {
x: number;
y: number;
// moveTo: (x: number, y: number) => bool;
moveTo(x: number, y: number): bool {};
}
The semicolon and the curly braces
or brackets
do not really add anything, just serves to confuse.
Yes!
interface IPoint {
x: number,
y: number,
// moveTo: (x: number, y: number) => bool
moveTo(x: number, y: number): bool
}
Keep it as close to Javascript Objects as possible, no need to pay the mental tax!
Try to DRY when describing your Types
and Interfaces
. Typescript type system is structural and not nominally which means that
a type matches any object that has the same structure or property members.
No!
interface IUserPosition {
x: number,
y: number
}
Yes!
interface IPoint {
x: number,
y: number
}
Return types#
You’re working with some method or function and you don’t know the return type to use. The first thing you should do is to check the documentation for the method or function, if part of the web api you could, for setTimeout’s example look at the doc ( https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout ) and look at Return value
.
You can also execute it on your console and check for Typeof
.
Otherwise, you can use ReturnType
to specify that the type is whatever the return type of foobar
is.
const myTimer: ReturnType<typeof setTimeout> = setTimeout(() => {
// Do something
}, 60)