Git Product home page Git Product logo

mesqueeb / is-what Goto Github PK

View Code? Open in Web Editor NEW
111.0 3.0 19.0 1.47 MB

JS type check (TypeScript supported) functions like `isPlainObject() isArray()` etc. A simple & small integration.

Home Page: https://npmjs.com/is-what

License: MIT License

JavaScript 2.88% TypeScript 97.12%
typechecker check-type javascript typescript javascript-type primitive-types plain-object plain-objects class-instance class-identifier

is-what's Introduction

is What? ๐Ÿ™‰

Total Downloads Latest Stable Version

Very simple & small JS type check functions. It's fully TypeScript supported!

npm i is-what

Or for deno available at: "deno.land/x/is_what"

Also check out is-where ๐Ÿ™ˆ

Motivation

I built is-what because the existing solutions were all too complex or too poorly built.

I was looking for:

  • A simple way to check any kind of type (including non-primitives)
  • Be able to check if an object is a plain object {} or a special object (like a class instance) โ€ผ๏ธ
  • Let TypeScript automatically know what type a value is when checking

And that's exactly what is-what is! (what a great wordplay ๐Ÿ˜ƒ)

Usage

is-what is really easy to use, and most functions work just like you'd expect.

// import functions you want to use like so:
import { isString, isDate, isPlainObject } from 'is-what'
  1. First I'll go over the simple functions available. Only isNumber and isDate have special treatment.
  2. After that I'll talk about working with Objects (plain objects vs class instances etc.).
  3. Lastly I'll talk about TypeScript implementation

Simple type check functions

// basics
isBoolean(true) // true
isBoolean(false) // true
isUndefined(undefined) // true
isNull(null) // true

// strings
isString('') // true
isEmptyString('') // true
isFullString('') // false

// numbers
isNumber(0) // true
isNumber('0') // false
isNumber(NaN) // false *
isPositiveNumber(1) // true
isNegativeNumber(-1) // true
// * see below for special NaN use cases!

// arrays
isArray([]) // true
isEmptyArray([]) // true
isFullArray([1]) // true

// objects
isPlainObject({}) // true *
isEmptyObject({}) // true
isFullObject({ a: 1 }) // true
// * see below for special object (& class instance) use cases!

// functions
isFunction(function () {}) // true
isFunction(() => {}) // true

// dates
isDate(new Date()) // true
isDate(new Date('invalid date')) // false

// maps & sets
isMap(new Map()) // true
isSet(new Set()) // true
isWeakMap(new WeakMap()) // true
isWeakSet(new WeakSet()) // true

// others
isRegExp(/\s/gi) // true
isSymbol(Symbol()) // true
isBlob(new Blob()) // true
isFile(new File([''], '', { type: 'text/html' })) // true
isError(new Error('')) // true
isPromise(new Promise((resolve) => {})) // true

// primitives
isPrimitive('') // true
// true for any of: boolean, null, undefined, number, string, symbol

Let's talk about NaN

isNaN is a built-in JS Function but it really makes no sense:

// 1)
typeof NaN === 'number' // true
// ๐Ÿค” ("not a number" is a "number"...)

// 2)
isNaN('1') // false
// ๐Ÿค” the string '1' is not-"not a number"... so it's a number??

// 3)
isNaN('one') // true
// ๐Ÿค” 'one' is NaN but `NaN === 'one'` is false...

With is-what the way we treat NaN makes a little bit more sense:

import { isNumber, isNaNValue } from 'is-what'

// 1)
isNumber(NaN) // false!
// let's not treat NaN as a number

// 2)
isNaNValue('1') // false
// if it's not NaN, it's not NaN!!

// 3)
isNaNValue('one') // false
// if it's not NaN, it's not NaN!!

isNaNValue(NaN) // true

isPlainObject vs isAnyObject

Checking for a JavaScript object can be really difficult. In JavaScript you can create classes that will behave just like JavaScript objects but might have completely different prototypes. With is-what I went for this classification:

  • isPlainObject will only return true on plain JavaScript objects and not on classes or others
  • isAnyObject will be more loose and return true on regular objects, classes, etc.
// define a plain object
const plainObject = { hello: 'I am a good old object.' }

// define a special object
class SpecialObject {
  constructor(somethingSpecial) {
    this.speciality = somethingSpecial
  }
}
const specialObject = new SpecialObject('I am a special object! I am a class instance!!!')

// check the plain object
isPlainObject(plainObject) // returns true
isAnyObject(plainObject) // returns true
getType(plainObject) // returns 'Object'

// check the special object
isPlainObject(specialObject) // returns false !!!!!!!!!
isAnyObject(specialObject) // returns true
getType(specialObject) // returns 'Object'

Please note that isPlainObject will only return true for normal plain JavaScript objects.

Getting and checking for specific types

You can check for specific types with getType and isType:

import { getType, isType } from 'is-what'

getType('') // returns 'String'
// pass a Type as second param:
isType('', String) // returns true

If you just want to make sure your object inherits from a particular class or toStringTag value, you can use isInstanceOf() like this:

import { isInstanceOf } from 'is-what'

isInstanceOf(new XMLHttpRequest(), 'EventTarget')
// returns true
isInstanceOf(globalThis, ReadableStream)
// returns false

TypeScript

is-what makes TypeScript know the type during if statements. This means that a check returns the type of the payload for TypeScript users.

function isNumber(payload: any): payload is number {
  // return boolean
}
// As you can see above, all functions return a boolean for JavaScript, but pass the payload type to TypeScript.

// usage example:
function fn(payload: string | number): number {
  if (isNumber(payload)) {
    // โ†‘ TypeScript already knows payload is a number here!
    return payload
  }
  return 0
}

isPlainObject and isAnyObject with TypeScript will declare the payload to be an object type with any props:

function isPlainObject(payload: any): payload is { [key: string]: any }
function isAnyObject(payload: any): payload is { [key: string]: any }
// The reason to return `{[key: string]: any}` is to be able to do
if (isPlainObject(payload) && payload.id) return payload.id
// if isPlainObject() would return `payload is object` then it would give an error at `payload.id`

isObjectLike

If you want more control over what kind of interface/type is casted when checking for objects.

To cast to a specific type while checking for isAnyObject, can use isObjectLike<T>:

import { isObjectLike } from 'is-what'

const payload = { name: 'Mesqueeb' } // current type: `{ name: string }`

// Without casting:
if (isAnyObject(payload)) {
  // in here `payload` is casted to: `Record<string | number | symbol, any>`
  // WE LOOSE THE TYPE!
}

// With casting:
// you can pass a specific type for TS that will be casted when the function returns
if (isObjectLike<{ name: string }>(payload)) {
  // in here `payload` is casted to: `{ name: string }`
}

Please note: this library will not actually check the shape of the object, you need to do that yourself.

isObjectLike<T> works like this under the hood:

function isObjectLike<T extends object>(payload: any): payload is T {
  return isAnyObject(payload)
}

Meet the family (more tiny utils with TS support)

Source code

It's litterally just these functions:

function getType(payload) {
  return Object.prototype.toString.call(payload).slice(8, -1)
}
function isUndefined(payload) {
  return getType(payload) === 'Undefined'
}
function isString(payload) {
  return getType(payload) === 'String'
}
function isAnyObject(payload) {
  return getType(payload) === 'Object'
}
// etc...

See the full source code here.

is-what's People

Contributors

andarist avatar artemgovorov avatar dependabot[bot] avatar hunterliu1003 avatar jasonhk avatar jcbhmr avatar laquasicinque avatar mesqueeb avatar msealand avatar rigidoont avatar thewilkybarkid avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

is-what's Issues

Add automation

Original issue

GitHub Actions can do a lot of magic stuff for you! Here's some things that a lot of npm packages have that might be interesting to add to this project:

  1. A test workflow that runs npm test on each Node LTS version
  2. A publish workflow to auto run npm publish with an npm token on each release

Things to automate:

  • Make it so that creating a GitHub Release does these things:
    • Publish to https://www.npmjs.com/ using npm publish
    • Publish to https://deno.land/x using another deno/-prefixed release tag on a build branch
    • Deploy the typedoc generated documentation website to GitHub Pages
  • Make it so that PRs have the following tests run against them:
    • Run npm test on Node.js LTS versions
    • Make sure that the package.json version property has changed
  • Make it so that PRs that change documentation-related things have a preview artifact that you can inspect

[Discussion] Named vs default exports

More discussion on the pros and cons of default exports vs named exports has been discussed to death in airbnb/javascript#1365 where Airbnb explains why they use default exports

it's a bit much to parse through. I am not seeing where exactly AirBnB's reasoning is in this thread. But a quick glance and this caught my eye:

  • Named exports make it clear throughout the application that you are using a specific value. Using default exports means that each part of the app may have a different name for the same function which makes code harder to reason about.
  • The argument about changing the name requiring all other parts of the application needing to do the same WAS VALID at one point, but with features like Rename Symbol which most IDE's - and VSCode (which lets be real most of us are using) have -- this became 100% useless.
  • Named exports make things like Auto Imports that some language features such as TypeScript support. Without named exports, they are far more prone to issue and require context before they are ever capable of suggesting an auto import of a given value.
  • If you end up needing to export more values from a file as the application grows, it ends up becoming quite messy to either change from default to named as this rule seems to want -- or move to more files being used. With the more files approach - if you want to keep things logically organized that may even mean you have to move things into a folder and make significant changes to the design of the project as it grows and can end up highly disorganized
    Whereas if you default to named exports then nothing changes, you simply export more values. If you want to use folder approach, you can - just do the folder, create an index file and export named exports from that - and dependent files have no knowledge of the change required.

This is kinda where I'm at. This and also all of the issues I've had in the past. Literally 10+ hours wasted in tooling hell and it all went away by using named exports.

Originally posted by @mesqueeb in #57 (comment)

Consider publishing solely to npm, and recommending Deno use npm:is-what?

image
https://www.infoworld.com/article/3680389/deno-stabilizes-npm-compatibility.html

Would this be a viable option? With Deno introducing npm: specifiers last year, I don't know if it's necessary to keep two package publications alive.

@mesqueeb What's the current process for deploying? Do you manually run npm publish? What happens? How do you deploy to deno.land?

Cool tooling:
https://www.denoify.land/
https://github.com/denoland/publish-folder
https://docs.denoify.land/publishing-on-deno.land-x

More edge-case tests

Example for things like isString():

  • Object.create(String.prototype)
  • new String()
  • new class extends String {}
  • new iframe.contentWindow.String()
  • { [Symbol.toStringTag]: "String" }
  • new class String {}
  • new class String { [Symbol.toStringTag] = "String" }
  • new (iframe.contentWindow.eval("class String { [Symbol.toStringTag] = 'String' }"))()

@mesqueeb Thoughts on what each of these edge-cases should return?

Is function isObjectLike checks the type?

From documentation I am not quite understanding what really do isObjectLike?

Documentation says Returns whether the payload is an object like a type passed in < >

I can see that If I passed not an object then it will return the false:

isObjectLike<{a: string}>(true) // false

But If I passed some object it will return the true any way:

isObjectLike<{a: string}>({b: 3}) // true

Link to TS playgound

add JSONObject type

type Primitive =
  | bigint
  | boolean
  | null
  | number
  | string
  | symbol
  | undefined;

type JSONValue = Primitive | JSONObject | JSONArray;

interface JSONObject {
  [key: string]: JSONValue;
}

interface JSONArray extends Array<JSONValue> { }

Expose individual files as exported files

Basically add this to package.json

{
  "exports": {
    ".": "./dist/index.js",
    // In addition to โ˜
    "./*.js": "./dist/*.js"
  }
}

to allow:

import { isObject } from "is-what/isObject.js"

or if we used default exports

import isObject from "is-what/isObject.js"

this is in the spirit of keeping the package as light as possible. This way, even if your target environment doesn't do bundling/tree-shaking, you can still manually "treeshake" in your code to only import what's needed.

cc @mesqueeb is this a good idea?

[Milestone] v5

Here's a wishlist of things that I'd like to see for a v5 release of this package:

  • #47
  • #69
  • #79
  • Remove the "dev" branch (it's a bit redundant since things get merged to "main"/"production" anyways)
  • Remove the "legacy" branch -- What is it for? ๐Ÿค”
  • Make it so that creating a GitHub Release is the main way to create a new version
  • #40
  • Improve the readme & take advantage of docs site
    • Move "isPlainObject() vs isAnyObject()" to a TypeDoc page
  • #45
  • #50
  • #71
  • #59
  • Fix #37 (would be auto-fixed if ESM-only)
  • #44
  • Use plain tsc

@mesqueeb Thoughts?

Split src/index.ts into multiple files

Attempting to be tackled in #57. This is the discussion portion so that the PR is about the "how" and "what" and this issue is about the "why" "who" and "when". ๐Ÿ˜‰

This is a good idea because:

  1. Having one thing per file encourages you to fill that screen space with DOCS!
  2. Having one thing per file is general good practice to compartmentalize things
  3. You can more easily see what specific functions are touched by diffs or PRs since each one is its own file!
  4. Some popular JS styleguides say that this is a good idea ๐Ÿคทโ€โ™€๏ธ

Interested in @tinylibs?

I can't speak on @mesqueeb your behalf, but I think that @tinylibs seems to have a pretty similar goal to yours with the suite of small TS utils:

image

I think that mentioning them in the readme could be a good idea! ๐Ÿ˜Š

[Discussion] Consider using stricter brand checks than just Symbol.toStringTag?

Right now if you use something like a math library that defines a Symbol class for math operations and do isSymbol() on it, there is a serious non-zero chance that it will return true! ๐Ÿ˜ฑ

FancyMath.Symbol = class Symbol {
  constructor(expression: string, id: string) {
    // ...
  }

  [Symbol.toStringTag] = "Symbol"
}

const x = new FancyMath.Symbol("x + y", "x")
console.log(isSymbol(x))
//=> true

This happens because isSymbol() relies on Symbol.toStringTag via Object#toString() to "sniff" the type. There are other more robust ways for this particular type, though, and I think they should be used.

Example:

// Returns TRUE for Symbol() primitives
// Returns TRUE for Object(Symbol()) boxed primitives
// Returns TRUE for Object.create(Symbol.prototype)
// Returns TRUE for classes that extend Symbol
// Returns FALSE for the FancyMath.Symbol
// Returns TRUE for FancyMath.Symbol if it's from another realm
function isSymbol(x) {
  if (typeof x === "symbol") {
    return true
  } else if (x && typeof x === "object") {
    if (x instanceof Object) {
      // Same realm object
      return x instanceof Symbol
    } else if (!Object.getPrototypeOf(x)) {
      // Null-prototype object
      return false
    } else {
      // Object that is probably cross-realm
      return Object.prototype.toString.call(x).slice(8, -1) === "Symbol"
    }
  } else {
    return false
  }
}

Or, you could get fancier and do a branch check on the .description getter! If it throws, it's not a symbol.

// Returns TRUE for Symbol() primitives
// Returns TRUE for Object(Symbol()) boxed primitives
// Returns FALSE for Object.create(Symbol.prototype)
// Returns TRUE for classes that extend Symbol
// Returns FALSE for the FancyMath.Symbol
// Returns idk for FancyMath.Symbol if it's from another realm
function isSymbol(x) {
  try {
    Object.getOwnPropertyDescriptor(Symbol.prototype, "description").get.call(x)
  } catch {
    return false
  }
  return true
}

Move FUNDING.yml to .github repo

Did you know: you can have default community health files! ๐Ÿ˜Š That means you can put a single FUNDING.yml in your user/.github repository and it will automagically apply to all your repositories!

https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/creating-a-default-community-health-file

You can add default community health files to a public repository called .github, in the root of the repository or in the docs or .github folders.

@mesqueeb I notice you don't have a .github repo yet: https://github.com/mesqueeb/.github is a 404. I think this is a great time to create one! โค๏ธ

For reference, here's my .github repo: https://github.com/jcbhmr/.github

Add benchmarks

Something like https://github.com/tinylibs/tinybench#usage to get a feel for how fast calling these functions (with their recursive delegation and type guards) is compared to just typeof thing === "object" || typeof thing === "function" inline in your JS code. Basically a quick "how much tax?" answer via a benchmark.

something like:

// test/index.bench.ts
import { Bench } from "tinybench";
import { getType, isAnyObject, isPlainObject } from "is-what"; // ../src/index

let i = 0;
const objects = [{}, "", new Number(43), null, globalThis];
const bench = new Bench({ time: 100 });

bench.add("getType()", () => getType(objects[(i = i + (1 % 5))]));
bench.add("inline toString.call().slice()", () =>
  Object.prototype.toString.call(objects[(i = i + (1 % 5))]).slice(8, -1)
);

bench.add("isPlainObject()", () => isPlainObject(objects[(i = i + (1 % 5))]));
bench.add(
  "inline 2x __proto__",
  () => objects[(i = i + (1 % 5))]?.__proto__?.__proto__ == null
);

bench.add("isAnyObject()", () => isAnyObject(objects[(i = i + (1 % 5))]));
bench.add("inline typeof", () => {
  const o = objects[(i = i + (1 % 5))];
  return o && (typeof o === "object" || typeof o === "function");
});

await bench.run();
console.table(bench.table());

image

๐Ÿ‘† Showing off how LOW TAX this library is would be a good idea IMO! ๐Ÿ‘

Use `unknown` instead of `any`

CURRENTLY

We currently use any a lot like. Eg.:

image

This currently makes stuff like this possible without error:

image

PROPOSAL

When the user passes an array with "any" we could also convert the outcome to "unknown" instead like so:

image

Which will result in throwing errors because the type was converted from any to unknown:

image

As long as the array type is not any[] it will keep the array type as is:

image

@laquasicinque thoughts?

Dual package hazard

I hit the dual package hazard! ๐Ÿคฃ Which means there are duplicate functions in my bundle because I use import {} from "is-what" and then one of my dependencies uses require("is-what") ๐Ÿ˜ฑ

image

https://nodejs.org/api/packages.html#dual-package-hazard
๐Ÿ‘‡ A possible solution as suggested by Node.js docs โ˜

image

Then again, this might not be an issue! ๐Ÿ˜†

Consider isInstanceOf() based on string tags?

A reasonably popular npm package is https://github.com/lamansky/is-instance-of (1k downloads last week), but it's severely outdated. I think this package would be a great place for such a function? Or maybe there already is one that does what I'm looking for and I couldn't find it?

Example use case:

// Covers ServiceWorkerGlobalScope, DedicatedWorkerGlobalScope,
// plain WorkerGlobalScope from web-worker polyfill, SharedWorkerGlobalScope, etc.
if (isInstanceOf(globalThis, "WorkerGlobalScope")) {
  onmessage = (e) => console.log(e.data);
  doThingInWorker(self);
}
doNormalThing();

vs the old way

for (let p = globalThis; p; p = Object.getPrototypeOf(p)) {
  if (Object.prototype.toString.call(p).slice(8, -1) === "WorkerGlobalScope") {
    onmessage = (e) => console.log(e.data);
    doThingInWorker(self);
    break;
  }
}
doNormalThing();

Better JSDoc with @example

Note that right now "is-what" is terrible for Google SEO since even refinements like "is-what npm" or "is-what github" all are like "What is GitHub?" and "What is npm? A quick intro." instead of this package. ๐Ÿคฃ I think that a website would be a good idea to make a presence for Googlers and also provide valuable documentation overview that is generated from code and doesn't need to be custom-made in the readme. Specifically, examples are CRUCIAL.

open question: will this ticket cover improving the SEO?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.