1
0
mirror of https://github.com/robonen/tools.git synced 2026-03-20 02:44:45 +00:00

feat(packages/stdlib): add union, string and collection types

This commit is contained in:
2025-02-02 04:48:31 +07:00
parent c68436a36a
commit c29cefdaac
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 './collections';
export * from './function';
export * from './promise';
export * from './string';
export * from './union';

View File

@@ -1,7 +1,18 @@
import { describe, expectTypeOf, it } from 'vitest';
import type { HasSpaces, Trim } from './string';
import type { HasSpaces, Trim, Stringable } from './string';
describe('string', () => {
describe('Stringable', () => {
it('should be a string', () => {
expectTypeOf(Number(1)).toMatchTypeOf<Stringable>();
expectTypeOf(String(1)).toMatchTypeOf<Stringable>();
expectTypeOf(Symbol()).toMatchTypeOf<Stringable>();
expectTypeOf(new Array(1)).toMatchTypeOf<Stringable>();
expectTypeOf(new Object()).toMatchTypeOf<Stringable>();
expectTypeOf(new Date()).toMatchTypeOf<Stringable>();
});
});
describe('Trim', () => {
it('remove leading and trailing spaces from a string', () => {
type actual = Trim<' hello '>;

View File

@@ -1,3 +1,10 @@
/**
* Stringable type
*/
export interface Stringable {
toString(): string;
}
/**
* 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;