DEV Community

Cover image for How to make it easier for yourself to develop using types-spring
Sanshain
Sanshain

Posted on

How to make it easier for yourself to develop using types-spring

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
Enter fullscreen mode Exit fullscreen mode

Also we should have at least 4.7.4 typescript version. If we havn't - make it:

npm i -D typescript@latest
Enter fullscreen mode Exit fullscreen mode

Then we need to add the package to include section of our tsconfig.json:

    "include": [
        "./sources/index.ts",
        "./node_modules/types-spring/**/*"
    ]   
Enter fullscreen mode Exit fullscreen mode

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[]
Enter fullscreen mode Exit fullscreen mode

After:

const a = [1, 2, 3] as const;         // [1, 2, 3]
let arr = a.map(r => r + '')          // [string, string, string]
Enter fullscreen mode Exit fullscreen mode

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!
}
Enter fullscreen mode Exit fullscreen mode

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 
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Object.assign

Before:

let t = Object.assign({ a: 7, b: 8 }, { b: '' }) 
// => {a: number, b: never}
Enter fullscreen mode Exit fullscreen mode

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}

Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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)