1
0
mirror of https://github.com/robonen/tools.git synced 2026-03-20 19:04:46 +00:00

feat(core/stdlib): add type definitions and tests for collections and union types

This commit is contained in:
2025-05-20 19:19:41 +07:00
parent f7312b1060
commit 890d984aad
7 changed files with 161 additions and 1 deletions

View 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>();
});
});
});

View 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;

View File

@@ -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';

View File

@@ -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 '>;

View File

@@ -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`
*/ */

View 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>();
});
});
});

View 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;