Typescript is great and we know that. However each of us sometimes faces cases when built-in types works slightly loose. May be even any of us listened about package ts-reset, which one fixes the most annoying | unsafe points in typescript. But now we will talk about another little-known awesome package called types-spring. Aim of it is also to eliminate some of the shortcomings of the built-in types in typescipt and also deliver additional utility types that will facilitate daily work.
It's includes type safe enhancements that were not provided by ts-reset package, but which we face in daily development. The package contains improvements in types, both the built-in javascript standard functions and DOM methods. Well, less words, more action. Let's see what concretely it proffers:
First of all, install this package and tune it:
npm i -D types-spring
Also we should have at least 4.7.4 typescript version. If we havn't - make it:
npm i -D typescript@latest
Then we need to add the package to include
section of our tsconfig.json
:
"include": [
"./sources/index.ts",
"./node_modules/types-spring/**/*"
]
That's all. So we are ready to get started:
Built-in types features:
Array.map
Before:
const a = [1, 2, 3] as const; // [1, 2, 3]
let arr = a.map(r => r + '') // string[]
After:
const a = [1, 2, 3] as const; // [1, 2, 3]
let arr = a.map(r => r + '') // [string, string, string]
As we can see, with the types-string patch, a more exact array type was deduced.
Array.isArray
Before:
function checkArray(a: { a: 1 } | ReadonlyArray<number>)
{
if (Array.isArray(a)) {
// inside this block `a` variable will have type `any[]` - that's not quite exact. We know `any` is a free type without type checking. So it can lead to either runtime errors:
a.forEach(item => item.f()) // => runtime error!
}
else { a.a } // type error: property `a` does not exists!
}
After:
function checkArray(a: { a: 1 } | ReadonlyArray<number>)
{
if (Array.isArray(a)) {
// now `a` has type `number[]`. It's more correct then before anyway
a.forEach(item => item.f()) // type error: f does not exist on type number
}
else { a.a } // success
}
As we can see in first example (before) typescript's type guard works wrong. And next example with types-spring against has correct type inference.
Object.create
After:
let o = Object.create({}) // any
It's amazing, why typescript developers decided to return any
type from Object.create
function. Even we pass null
as argument - will have an object in runtime output (just with null prototype). Why would not return the object type instead? Type-spring do it:
let o = Object.create({}) // object
Object.assign
Before:
let t = Object.assign({ a: 7, b: 8 }, { b: '' })
// => {a: number, b: never}
As we could see the assign by default works wrong for matching keys with different types. But we know how it workds actually in runtime: the result will overwrite the old property with the new one. So correct behavior will be next:
let t = Object.assign({ a: 7, b: 8 }, { b: '' })
// => {a: number, b: string}
Types-spring does it. Not bad?
But also, no less, and perhaps even more interesting are the type patches that types-spring supplies for DOM:
DOM features:
querySelector
First of all it improves detecting Element type from selector signature:
Before:
const input = document.querySelector('input');
// input is HTMLInputElement | null
const unknown = document.querySelector('.cls');
// unknown is Element | null
const inputWCls = document.querySelector('input.cls');
// inputWCls is Element | null
if (divCls) {
inputWCls.value = '' // error
}
After:
const input = document.querySelector('input');
// input is also HTMLInputElement | null
const unknown = document.querySelector('.cls');
// unknown is also Element | null
const inputWCls = document.querySelector('input.cls');
// but inputWCls is HTMLInputElement | null
if (divCls) {
inputWCls.value = '' // success
}
As we could see original behavior extends the type of a specific element to Element if any selector other than the tag name is passed in the argument. But most of the time we don't use single tag selectors. because we need to somehow uniquely identify an element from other elements in the same tag on the page. In this case, we have to explicitly specify the type of the expected element as a generic. With types-string, if the argument starts with a tag selector, then regardless of which selectors are specified next, exactly the type of element whose tag is specified at the beginning will be output. Because... such a selector cannot return some other type of element.
HTMLElement.cloneNode
Before
const elem = document.getElementById('id') // elem is HTMLElement
const clonedElem = elem?.cloneNode() // clonedElem is Node
cloneNode
method clones source object as is. If it clones Node - so it'll be return a Node. But if it clones HTMLElement... obviosly it'll be return HTMLElement. So
After
const elem = document.getElementById('id') // elem is HTMLElement
const clonedElem = elem?.cloneNode() // clonedElem is HTMLElement
currentTarget
Types-spring makes automatic type detection for the currentTarget in MouseEvent, KeyboardEvent and other user interface events (just for addEventListener callback):
Before:
let elem: HTMLDivElement = document.querySelector('div');
elem?.addEventListener('click', e => {
let target = e.currentTarget; // is EventTarget | null
})
elem
is HTMLElement. So what kind of type may be currentEvent
inside its ui events? Right. It's it:
After
let elem: HTMLDivElement = document.querySelector('div');
elem?.addEventListener('click', e => {
let target = e.currentTarget; // is HTMLDivElement | null
})
Also besides the patches types-spring provides additional utility-types, which ones make the development even more efficient.
Conclusion
Types-string makes development safer and more efficient than without it. Of course, it doesn't solve all type problems, but at least it makes life easier. It can be used in combination with ts-reset or with other similar packages without any conflicts. What do you think about it?
Top comments (0)