mirror of
https://github.com/robonen/tools.git
synced 2026-03-20 10:54:44 +00:00
feat(core/stdlib): add type definitions and tests for collections and union types
This commit is contained in:
71
core/stdlib/src/types/ts/collections.test-d.ts
Normal file
71
core/stdlib/src/types/ts/collections.test-d.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { describe, expectTypeOf, it } from 'vitest';
|
||||||
|
import type { Path, PathToType } from './collections';
|
||||||
|
|
||||||
|
describe('collections', () => {
|
||||||
|
describe('Path', () => {
|
||||||
|
it('parse simple object path', () => {
|
||||||
|
type actual = Path<'user.name'>;
|
||||||
|
type expected = ['user', 'name'];
|
||||||
|
|
||||||
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parse simple array path', () => {
|
||||||
|
type actual = Path<'user.0'>;
|
||||||
|
type expected = ['user', '0'];
|
||||||
|
|
||||||
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parse complex object path', () => {
|
||||||
|
type actual = Path<'user.addresses.0.street'>;
|
||||||
|
type expected = ['user', 'addresses', '0', 'street'];
|
||||||
|
|
||||||
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parse double dot path', () => {
|
||||||
|
type actual = Path<'user..name'>;
|
||||||
|
type expected = ['user', '', 'name'];
|
||||||
|
|
||||||
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PathToType', () => {
|
||||||
|
it('convert simple object path', () => {
|
||||||
|
type actual = PathToType<['user', 'name']>;
|
||||||
|
type expected = { user: { name: unknown } };
|
||||||
|
|
||||||
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('convert simple array path', () => {
|
||||||
|
type actual = PathToType<['user', '0']>;
|
||||||
|
type expected = { user: unknown[] };
|
||||||
|
|
||||||
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('convert complex object path', () => {
|
||||||
|
type actual = PathToType<['user', 'addresses', '0', 'street']>;
|
||||||
|
type expected = { user: { addresses: { street: unknown }[] } };
|
||||||
|
|
||||||
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('convert double dot path', () => {
|
||||||
|
type actual = PathToType<['user', '', 'name']>;
|
||||||
|
type expected = { user: { '': { name: unknown } } };
|
||||||
|
|
||||||
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('convert to custom target', () => {
|
||||||
|
type actual = PathToType<['user', 'name'], string>;
|
||||||
|
type expected = { user: { name: string } };
|
||||||
|
|
||||||
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
28
core/stdlib/src/types/ts/collections.ts
Normal file
28
core/stdlib/src/types/ts/collections.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* A collection definition
|
||||||
|
*/
|
||||||
|
export type Collection = Record<PropertyKey, any> | any[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a collection path string into an array of keys
|
||||||
|
*/
|
||||||
|
export type Path<T> =
|
||||||
|
T extends `${infer Key}.${infer Rest}`
|
||||||
|
? [Key, ...Path<Rest>]
|
||||||
|
: T extends `${infer Key}`
|
||||||
|
? [Key]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a collection path array into a Target type
|
||||||
|
*/
|
||||||
|
export type PathToType<T extends string[], Target = unknown> =
|
||||||
|
T extends [infer Head, ...infer Rest]
|
||||||
|
? Head extends `${number}`
|
||||||
|
? Rest extends string[]
|
||||||
|
? PathToType<Rest, Target>[]
|
||||||
|
: never
|
||||||
|
: Rest extends string[]
|
||||||
|
? { [K in Head & string]: PathToType<Rest, Target> }
|
||||||
|
: never
|
||||||
|
: Target;
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
export * from './array';
|
export * from './array';
|
||||||
|
export * from './collections';
|
||||||
export * from './function';
|
export * from './function';
|
||||||
export * from './promise';
|
export * from './promise';
|
||||||
export * from './string';
|
export * from './string';
|
||||||
|
export * from './union';
|
||||||
|
|||||||
@@ -1,7 +1,18 @@
|
|||||||
import { describe, expectTypeOf, it } from 'vitest';
|
import { describe, expectTypeOf, it } from 'vitest';
|
||||||
import type { HasSpaces, Trim } from './string';
|
import type { HasSpaces, Trim, Stringable } from './string';
|
||||||
|
|
||||||
describe('string', () => {
|
describe('string', () => {
|
||||||
|
describe('Stringable', () => {
|
||||||
|
it('should be a string', () => {
|
||||||
|
expectTypeOf(Number(1)).toExtend<Stringable>();
|
||||||
|
expectTypeOf(String(1)).toExtend<Stringable>();
|
||||||
|
expectTypeOf(Symbol()).toExtend<Stringable>();
|
||||||
|
expectTypeOf(new Array(1)).toExtend<Stringable>();
|
||||||
|
expectTypeOf(new Object()).toExtend<Stringable>();
|
||||||
|
expectTypeOf(new Date()).toExtend<Stringable>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Trim', () => {
|
describe('Trim', () => {
|
||||||
it('remove leading and trailing spaces from a string', () => {
|
it('remove leading and trailing spaces from a string', () => {
|
||||||
type actual = Trim<' hello '>;
|
type actual = Trim<' hello '>;
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* Stringable type
|
||||||
|
*/
|
||||||
|
export interface Stringable {
|
||||||
|
toString(): string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trim leading and trailing whitespace from `S`
|
* Trim leading and trailing whitespace from `S`
|
||||||
*/
|
*/
|
||||||
|
|||||||
31
core/stdlib/src/types/ts/union.test-d.ts
Normal file
31
core/stdlib/src/types/ts/union.test-d.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { describe, expectTypeOf, it } from 'vitest';
|
||||||
|
import type { UnionToIntersection } from './union';
|
||||||
|
|
||||||
|
describe('union', () => {
|
||||||
|
describe('UnionToIntersection', () => {
|
||||||
|
it('convert a union type to an intersection type', () => {
|
||||||
|
type actual = UnionToIntersection<{ a: string } | { b: number }>;
|
||||||
|
type expected = { a: string } & { b: number };
|
||||||
|
|
||||||
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('convert a union type to an intersection type with more than two types', () => {
|
||||||
|
type actual = UnionToIntersection<{ a: string } | { b: number } | { c: boolean }>;
|
||||||
|
type expected = { a: string } & { b: number } & { c: boolean };
|
||||||
|
|
||||||
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('no change when the input is already an intersection type', () => {
|
||||||
|
type actual = UnionToIntersection<{ a: string } & { b: number }>;
|
||||||
|
type expected = { a: string } & { b: number };
|
||||||
|
|
||||||
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('never when union not possible', () => {
|
||||||
|
expectTypeOf<UnionToIntersection<string | number>>().toEqualTypeOf<never>();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
10
core/stdlib/src/types/ts/union.ts
Normal file
10
core/stdlib/src/types/ts/union.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* Convert a union type to an intersection type
|
||||||
|
*/
|
||||||
|
export type UnionToIntersection<Union> = (
|
||||||
|
Union extends unknown
|
||||||
|
? (distributedUnion: Union) => void
|
||||||
|
: never
|
||||||
|
) extends ((mergedIntersection: infer Intersection) => void)
|
||||||
|
? Intersection & Union
|
||||||
|
: never;
|
||||||
Reference in New Issue
Block a user