Skip to content

Commit

Permalink
feat: add isClass function (#239)
Browse files Browse the repository at this point in the history
  • Loading branch information
MarlonPassos-git authored and radashi-bot committed Nov 11, 2024
1 parent 9b315a9 commit fe11bf5
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 0 deletions.
11 changes: 11 additions & 0 deletions benchmarks/typed/isClass.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as _ from 'radashi'

describe('isClass', () => {
bench('with class', () => {
_.isClass(class CustomClass {})
})

bench('with non-class', () => {
_.isClass({})
})
})
33 changes: 33 additions & 0 deletions docs/typed/isClass.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
title: isClass
description: Determine if a value was declared with `class` syntax
---

### Usage

This function returns `true` if the provided value is a constructor declared with the ES6 `class` keyword.

```ts
import * as _ from 'radashi'

class MyClass {}

_.isClass(MyClass) // => true
_.isClass(Error) // => false
_.isClass(function OldClass() {}) // => false
_.isClass('abc') // => false
_.isClass({}) // => false
_.isClass(undefined) // => false
```

:::note

Old school constructors (declared with the `function` keyword) will return `false`.

Built-in class constructors (e.g. `Error`) will also return `false`, because they're created with native code, not the `class` keyword.

:::

### Popular use cases

- Type guard would check for a function that must be called with the `new` operator
1 change: 1 addition & 0 deletions src/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export * from './string/trim.ts'

export * from './typed/isArray.ts'
export * from './typed/isBoolean.ts'
export * from './typed/isClass.ts'
export * from './typed/isDate.ts'
export * from './typed/isEmpty.ts'
export * from './typed/isEqual.ts'
Expand Down
34 changes: 34 additions & 0 deletions src/typed/isClass.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { isFunction, type Class, type StrictExtract } from 'radashi'

/**
* Checks if the given value is a class. This function verifies
* if the value was defined using the `class` syntax. Old school
* classes (defined with constructor functions) will return false.
* "Native classes" like `Error` will also return false.
*
* @see https://radashi.js.org/reference/typed/isClass
* @example
* ```ts
* isClass(class CustomClass {}) // => true
* isClass('abc') // => false
* isClass({}) // => false
* ```
*/
export function isClass<T>(value: T): value is ExtractClass<T> {
return (
isFunction(value) &&
Function.prototype.toString.call(value).startsWith('class ')
)
}

/**
* Used by the `isClass` type guard. It handles type narrowing for
* class constructors and even narrows `any` types.
*/
export type ExtractClass<T> = [StrictExtract<T, Class>] extends [Class]
? Extract<T, Class>
: T extends any
? Class<unknown[], unknown> extends T
? Class<unknown[], unknown>
: never
: never
7 changes: 7 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ export declare class Any {
private any: typeof any
}

/**
* Represents a class constructor.
*/
export type Class<TArgs extends any[] = any[], TReturn = any> = new (
...args: TArgs
) => TReturn

/**
* Extracts `T` if `T` is not `any`, otherwise `never`.
*
Expand Down
45 changes: 45 additions & 0 deletions tests/typed/isClass.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { Class } from 'radashi'
import * as _ from 'radashi'
import { expectTypeOf } from 'vitest'

declare class Person {
name: string
}

describe('isClass', () => {
test('value is union containing a class type', () => {
const value = {} as Person | typeof Person
if (_.isClass(value)) {
expectTypeOf(value).toEqualTypeOf<typeof Person>()
expectTypeOf(new value()).toEqualTypeOf<Person>()
} else {
expectTypeOf(value).toEqualTypeOf<Person>()
}
})
test('value is unknown', () => {
const value = {} as unknown
if (_.isClass(value)) {
expectTypeOf(value).toEqualTypeOf<Class<unknown[], unknown>>()
expectTypeOf(new value()).toEqualTypeOf<unknown>()
} else {
expectTypeOf(value).toEqualTypeOf<unknown>()
}
})
test('value is any', () => {
const value = {} as any
if (_.isClass(value)) {
expectTypeOf(value).toEqualTypeOf<Class<unknown[], unknown>>()
expectTypeOf(new value()).toEqualTypeOf<unknown>()
} else {
expectTypeOf(value).toEqualTypeOf<any>()
}
})
test('value is string', () => {
const value = {} as string
if (_.isClass(value)) {
expectTypeOf(value).toEqualTypeOf<never>()
} else {
expectTypeOf(value).toEqualTypeOf<string>()
}
})
})
39 changes: 39 additions & 0 deletions tests/typed/isClass.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as _ from 'radashi'

function OldSchoolClass(something: string) {
// @ts-ignore
this.something = something
}
OldSchoolClass.prototype.doSomething = function () {
return `do ${this.something}`
}

describe('isClass', () => {
test('returns false for non-Class values', () => {
const fn = () => {}

class MyFunction extends Function {
toString() {
return 'class instance of MyFunction'
}
}

expect(_.isClass(OldSchoolClass)).toBeFalsy()
expect(_.isClass(fn)).toBeFalsy()
expect(_.isClass(undefined)).toBeFalsy()
expect(_.isClass(null)).toBeFalsy()
expect(_.isClass(false)).toBeFalsy()
expect(_.isClass(() => {})).toBeFalsy()
expect(_.isClass(async () => {})).toBeFalsy()
expect(_.isClass(new MyFunction())).toBeFalsy()
expect(_.isClass(Number.NaN)).toBeFalsy()
expect(_.isClass([1, 2, 3])).toBeFalsy()
expect(_.isClass({})).toBeFalsy()
expect(_.isClass('abc')).toBeFalsy()
expect(_.isClass(String('abc'))).toBeFalsy()
})
test('returns true for class values', () => {
expect(_.isClass(class CustomError extends Error {})).toBeTruthy()
expect(_.isClass(class CustomClass {})).toBeTruthy()
})
})

0 comments on commit fe11bf5

Please sign in to comment.