mirror of
https://github.com/robonen/tools.git
synced 2026-03-20 02:44:45 +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 './collections';
|
||||
export * from './function';
|
||||
export * from './promise';
|
||||
export * from './string';
|
||||
export * from './union';
|
||||
|
||||
@@ -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)).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', () => {
|
||||
it('remove leading and trailing spaces from a string', () => {
|
||||
type actual = Trim<' hello '>;
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* Stringable type
|
||||
*/
|
||||
export interface Stringable {
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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