gvergnaud / hotscript Goto Github PK
View Code? Open in Web Editor NEWA library of composable functions for the type-level! Transform your TypeScript types in any way you want using functions you already know.
A library of composable functions for the type-level! Transform your TypeScript types in any way you want using functions you already know.
In version 1.0.11
, Objects.Omit
and Objects.Pick
make all properties required. It also ignores readonly
and make everything writable
https://codesandbox.io/s/typescript-playground-export-forked-5y6rtd
import { Pipe, Objects, Identity } from "hotscript";
type HasNoOptionals1 = Pipe<
// ^?
{
readonly a: string;
b: number;
c?: boolean;
d?: number;
},
[Objects.Omit<"a">]
>;
type HasNoOptionals2 = Pipe<
// ^?
{
readonly a: string;
b: number;
c?: boolean;
d?: number;
},
[Objects.Omit<"">]
>;
type HasNoOptionals3 = Pipe<
// ^?
{
readonly a: string;
b: number;
c?: boolean;
d?: number;
},
[Objects.Pick<"a" | "c">]
>;
type HasNoOptionals4 = Pipe<
// ^?
{
readonly a: string;
b: number;
c?: boolean;
d?: number;
},
[Objects.Pick<"a" | "b" | "c" | "d">]
>;
type WithIdentity = Pipe<
// ^?
{
readonly a: string;
b: number;
c?: boolean;
d?: number;
},
[Identity]
>;
export {};
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
"strictBindCallApply": true,
"noImplicitThis": true,
"noImplicitReturns": true,
"alwaysStrict": true,
"esModuleInterop": true,
"declaration": true,
"target": "ES2017",
"jsx": "react",
"module": "ESNext",
"moduleResolution": "node"
}
}
Playground Link: Provided
Hi!
I noticed that Negate<0>
returns number | bigint
and I'd expect it to return 0
(doesn't seem like TypeScript has -0
, which is fine by me).
import { Call, Numbers } from 'hotscript'
type ShouldBeZero = Call<Numbers.Negate<0>>
// ^?
type Zero = -0;
// ^?
The feature request is to make this possible:
$<Objects.update<'path', 1>, {path?:2}> // $ExpectType {path: 1}
That would be a breaking change, and doesn't support going the other direction. So probably a new function like this:
$<Objects.update<'path', 1>, Objects.updateSpecifier<'-?', 'path'>, {path?:2}> // $ExpectType {path: 1}
Document says
interface IsNumber extends Fn {
return: this["arg0"] extends number ? true : false;
}
type T0 = Call<Tuples.GroupBy<IsNumber>, [1, "str", 2]>;
// ^? { true: [1, 2], false: ["str"] }
type T2 = Call<Tuples.GroupBy<Strings.StartsWith<"a">>, ["alice", "bob", "carl"]>;
// ^? { true: ["alice"], false: ["bob", "carl"] }
But it's actually
interface IsNumber extends Fn {
return: this['arg0'] extends number ? true : false;
}
type T0 = Call<Tuples.GroupBy<IsNumber>, [1, 'str', 2]>;
// never
type T2 = Call<Tuples.GroupBy<Strings.StartsWith<'a'>>, ['alice', 'bob', 'carl']>;
// never
Because boolean can not be the property name
type MyDictionary = {
[key: string]: {
name: string;
age: number;
};
};
export type Paths = Call<Objects.AllPaths, MyDictionary>; // never
Expected behaviour IMO would be string | "${string}.name" | "${string}.age"
. Is this possible?
Re #41
I could be mistaken, but I don't think Unions.Range
includes the final element. In other words, Unions.Range<1, 3>
would match 1
and 2
, but not 3
.
Including the final element by default would be more intuitive in my opinion, and would only require a small adjustment to the function (something like | EndValue
).
There is a function Objects.Partial
but it turns all properties on an object to optional. Would be nice to have a version that targets a specific property: Objects.SetPropertyOptional<'foo', SomeObject>
.
When working with type computation, it is generally useful to use a "simplify" type utility to compute the final result so that e.g. IDEs show something more useful to end-users.
Example: https://github.com/sindresorhus/type-fest/blob/main/source/simplify.d.ts
It would be nice if Hotscript ships with its own version, as such a utility is often needed for the kinds of complex use-cases one would reach for Hotscript for.
I think it would help to add a Simplify
type function to reduce(expand) types, like this:
type Flatten<T> = T extends Function ? T : T extends object ? { [K in keyof T]: Flatten<T[K]> } : T;
export type Simplify<T> = Flatten<T> & {};
interface SomeData {
a: number;
b?: boolean;
}
type SomeMoreData = SomeData & {
c: string;
}
type WithoutSimplify = SomeMoreData; // -> SomeData & { c: string; }
type WithSimplify = Simplify<SomeMoreData>; // -> { a: number; b?: boolean; c: string; }
I think an Enumerate
and Range
generic could be a nice addition to the Number/Tuple namespace.
I've been using these generics to generate lists of unioned numbers (maybe this could be a tuple instead of a union 🤷) for discriminated unions of objects (e.g. http responses discriminated by status code).
PS amazing work on this lib. I love the overall design and the DX truly looks fantastic!
Currently, trying to use a non-integer will only perform the operation on the integer portion of the number:
import { Apply, Numbers } from "hotscript";
type DoubleHalf = Apply<Numbers.Mul, [0.5, 2]>; // evaluates to 0
type DoubleTwoPointFive = Apply<Numbers.Mul, [2.5, 2]>; // evaluates to 4
Couldn't find any mention of it on this repo, so just was wondering if this is unintentional, planned to be implemented, or not being added?
Hey there! Thanks for the repo, it's very nice, just as your course (passed it and liked a lot!).
We at Evil Martians have plans to extend our nanostores ecosystem with a form library called nanoform. And for it we wanted to make a map store that will provide typesafe object path interface for the underlying structure. That's where we use Objects.Get
and Objects.AllPaths
utilities.
It works perfectly from the DX point of view, but we have an issue when building the lib. It's probably not hotscript specific, but I hoped you could give any guidance on how to fix that.
Here's a repro on StackBlitz. You need to run npm run build
in terminal there. See the problem is that tsc inlines a lot of hotscript code in the .d.ts
file making it unreadable mess.
What's extremely odd is that it does that for one function, getPath
, but not the other, setPath
, even though both use the same set of utility types. Do you have any idea how to get rid of this? Thanks!
There are several goals I am hoping to achieve here, with these type helpers:
Ensure a type is not another type
const a: string & Not<"test"> = "test";
// ^ TS should complain
Ensure a type is not in another grouped type like an array or object
Array example
const forbiddenStrings = ['bad', 'worse', 'worst'] as const;
const a: string & NotIn<typeof forbiddenStrings> = "bad";
// ^ TS should complain
Object keys example
const forbiddenStrings = { bad: 1, worse: 2, worst: 3 } as const;
const a: string & NotIn<keyof typeof forbiddenStrings> = "bad";
// ^ TS should complain
Compare objects of different types to ensure none contain the same keys or elements (in the case of arrays)
const wordsA: string[] & NoIntersect<typeof wordsB, keyof typeof wordsC> = ['a', 'b', 'c'] as const;
// ^ TS should complain
const wordsB: string[] & NoIntersect<typeof wordsA, keyof typeof wordsC> = ['d', 'e', 'f'] as const;
// ^ TS should complain
const wordsC: string[] & NoIntersect<typeof wordsA, typeof wordsB> = { g: 1, h: 2, i: 3 } as const;
// ^ TS should complain
I'm' not sure what the API for this would look like, likely not what I showed above but something more argument-based like this…
type SomeType = NoIntersect<typeof wordsB, keyof typeof wordsC>;
…if the base type can be inferred, otherwise passed explicitly as the first arg like this:
type SomeType = NoIntersect<string[], typeof wordsB, keyof typeof wordsC>;
Something like this would be useful to be able to get the type at a given path, if possible.
/*
* { "a": { b: number }, "a.b": number }
*/
type Ret = Objects.PathTypeMap<{ a: { b: number } }>
It could be useful to match a number between a set of number, which a range function could be useful for.
type Enumerate<N extends number, Acc extends number[] = []> = Acc['length'] extends N
? Acc[number]
: Enumerate<N, [...Acc, Acc['length']]>;
type IntRange<F extends number, T extends number> = Exclude<Enumerate<Simplify<T>>, Enumerate<F>> | T;
type CustomRange = IntRange<20, 300>; // -> 20 | 21 | ... many more ... | 300
This type is inspired (99% copied) from this StackOverflow answer by AlexG, with a slight modification to include the last number in the series (| T
). Without this change, IntRange<20, 300>
would match 20
–299
instead or 20
–300
.
Code:
import { Call, Fn } from 'hotscript';
interface Just extends Fn {
return: this['arg0'];
}
function fn<T extends string>(a: T, b: Call<Just, T>) {
type JustString = Call<Just, String>;
const aLength = a.length;
const bLength = b.length;
const js: JustString = '';
const jsLength = js.length;
}
Error:
Property 'length' does not exist on type '(Equal<T, unique symbol> extends true ? [] : [T]) extends [infer arg, ...any[]] ? arg : never'.
11 const bLength = b.length;
~~~~~~
Our current function encoding creates a dichotomy between arguments received through Apply
(this["args"]
) and arguments passed explicitly via type parameters. This means every function needs to manually merge these two types of inputs, which is cumbersome and error prone.
In addition, we have a weird distinction between our "pipeable functions" that can't be called directly but only through Call
, Apply
or Eval
, and regular generic types that can be called directly, but aren't "pipeable".
Here is what I think could be a good solution:
Fn
extensions always receive their argument through Apply
, and read them using a new Fn.args<this>
function.Fn
extensions in type aliases that support partial inputs by using a new Curry
function.output
into return
, to stay close to the semantics of a value-level function.Pros:
MyFn<A, B, C>
Fn.args<this>
is easier to type than this["args"]
Fn.arg0<this>
is easier to type than this["args"][0]
/**
* Core
*/
export interface Fn {
args: unknown
return: unknown;
}
export type Apply<fn extends Fn, args extends unknown[]> = (fn & { args: args })["return"];
namespace Fn {
export type args<F> =
F extends { args: infer args extends unknown[] } ? RemoveUnknownArrayConstraint<args> : never;
export type arg0<F> =
F extends { args: [infer arg, ...any] } ? arg : never;
export type arg1<F> =
F extends { args: [any, infer arg, ...any] } ? arg : never
export type arg2<F> =
F extends { args: [any, any, infer arg, ...any] } ? arg : never
export type arg3<F> =
F extends { args: [any, any, any, infer arg, ...any] } ? arg : never
}
export type Pipe<acc, xs extends Fn[]> = xs extends [
infer first extends Fn,
...infer rest extends Fn[]
]
? Pipe<Apply<first, [acc]>, rest>
: acc;
export type placeholder = "@hotscript/placeholder";
export type unset = "@hotscript/unset";
export interface CurryImpl<fn extends Fn, partialArgs extends unknown[]>
extends Fn {
return: MergeArgs<Fn.args<this>, partialArgs> extends infer args extends unknown[]
? Apply<fn, args>
: never
}
type Curry<fn extends Fn, partialArgs extends unknown[]> =
unset extends partialArgs[number]
? CurryImpl<fn, partialArgs>
: placeholder extends partialArgs[number]
? CurryImpl<fn, partialArgs>
: Apply<fn, partialArgs>
/**
* Custom function
*/
/**
* Parameters are assigned with Fn.args, Fn.arg0, Fn.arg1, Fn.arg2, etc.
*/
interface ExcludeNumberFn extends Fn {
return: Fn.arg0<this> extends infer T ? T extends number ? never : T : never
// 👆
// Since we assigned T, this becomes
// A naked type variable, which means
// This expression distributes.
//
// It wouldn't with the simpler:
// `Fn.arg0<this> extends number ? never : Fn.arg0<this>`.
}
/**
* Wrap this in a call with curry:
*/
type ExcludeNumber<T = unset> = Curry<ExcludeNumberFn, [T]>
/**
* We can use it directly:
*/
type exclude1 = ExcludeNumber<"Gabriel" | 1 | 2 | "hello">
// ^? "Gabriel" | "hello"
/**
* Or through Apply!
*/
type exclude2 = Apply<ExcludeNumber, ["Gabriel" | 1 | 2 | "hello"]>
// ^? "Gabriel" | "hello"
/**
* It becomes more interesting with several parameters:
*/
interface PrependFn extends Fn {
// you don't have to merge piped args with partial args manually:
return: Fn.args<this> extends [infer Sep extends string, infer Str extends string]
? `${Sep}${Str}`
: never
}
// Because this is done by `Curry`
type Prepend<Sep = unset, Str = unset> = Curry<PrependFn, [Sep, Str]>
// You can partially apply and pipe:
type re3 = Pipe<"Hello", [
// ^? "start_start_start_Hello"
Prepend<"start_">,
Prepend<"start_">,
Prepend<"start_">,
]>
// You can use it directly:
type res2 = Prepend<"start", "end">
// ^? "startend"
// Here is what you get if you partially applied:
type res4 = Prepend<"start">
// ^? CurryImpl<PrependFn, ["start", "@hotscript/unset"]>
// And here what you get when you "pipe":
type res5 = Apply<Prepend<"start">, ["end"]>
// ^? "startend"
/**
* With this new encoding, we can also get rid of some weirdness
* We have with function of arity 2 like `Extends` when partially applied.
*/
// This is what you would expect:
export interface ExtendsFn extends Fn {
return: [Fn.arg0<this>] extends [Fn.arg1<this>] ? true : false
}
// and we handle the swapping of arguments here instead:
type Extends<A = unset, B = unset> =
Curry<ExtendsFn, B extends unset ? [unset, A] : [A, B]>
// 👆
// Swap if B is unset
type res6 = Extends<2, number>
// ^? true
type res7 = Pipe<"hello", [Extends<string>]>
// ^? true
// The arguments are swapped here
I also tried this approach of using a function type on Fn
to both assign arguments and compute the output:
export interface Fn {
args: unknown
fn(): unknown;
}
export type Apply<fn extends Fn, args extends unknown[]> =
ReturnType<(fn & { args: args })["fn"]>;
// 👆
// We need to get the return type
export interface ExtendsImpl extends Fn {
fn<A extends Fn.arg0<this>, B extends Fn.arg1<this>>():
// 👆 args are declared like this
[A] extends [B] ? true : false
// 👆 and this is the body of our function
}
// This stays the same
type Extends<A = unset, B = unset> =
Curry<ExtendsImpl, B extends unset ? [unset, A] : [A, B]>
type res6 = Extends<2, number>
// ^? true
type res7 = Pipe<"hello", [Extends<string>]>
// ^? true
pros:
ReturnType<...>
a lot, which could be bad for perfWe should add some tooling to easily compare how changes affect type-checking performance
Is there anyway to achieve something like https://github.com/sindresorhus/type-fest/blob/main/source/set-optional.d.ts without creating a new interface that extends from Fn
?
Hello, is it possible to inherit the return type dynamically of a given property? For example, let's assume we have a React component:
type InputProps = {
value: string
}
export const Input: FC<InputProps> = () => (<></>);
And then, we have the following type:
type FormField = {
component: Component | ForwardRefExoticComponent<any> | any;
props: ??? // it should get the return
}
const fields: FormField[] = [
{
component: Input,
props: { value: "test" }
}
]
I've been trying this from the past few hours and couldn't achieve any valuable results.
Best.
We should have an error path in all pipeable functions so that they can throw when their input is incorrect. Not sure what this would look like yet.
Keeping it simple, only takes into account values up to the minimum of both array's length
The paths for the object { a: 1, b: 2, c: [{ d: 3 }, { e: 4 }] } as const
are incorrect.
But it works correctly for { a: 1, b: 2, c: [{ d: 3 }, { e: 4 }] }
.
Reproduction:
https://stackblitz.com/edit/typescript-wrimqa?file=index.ts
Thanks for looking into this 👍
We maybe can setup Fn to be generic of input types. like so :
interface Fn<T=unknown> {
input: T;
output: unknown;
}
This allows to implement specialised algorithms (for arrays, strings, numbers) without having to type cast, exemple for pipe:
interface PipeFn extends Fn<{
acc: unknown;
item: Fn;
}> {
output: Call<this["input"]["item"], this["input"]["acc"]>
}
Making these actually easier to read
It seems the the readme is currently out of date on this and that T.Create is no longer part of the public API, at least as far as i can see. Is this correct?
The tuple trick to do number operations is really limited.
I have a complete implementation here with no limitations but ts memory and compute power :
https://github.com/ecyrbe/ts-calc
But it would be really out of scope, maybe ?
Sharing anyway just in case.
version: 1.0.11
typescript: 5.0.4
package.json
{
"name": "hot",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@types/node": "^18.16.0",
"hotscript": "^1.0.11",
"typescript": "^5.0.4"
}
}
tsconfig.json
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "nodenext",
"target": "ESNext",
"declaration": true,
"esModuleInterop": true,
"strict": true,
"outDir": "dist/"
},
"include": ["src/"]
}
tsconfig.json
{
"compilerOptions": {
"declaration": true,
"esModuleInterop": true,
"strict": true,
"outDir": "dist/"
},
"include": ["src/"]
}
hotscript does not appear to work with these settings
...
"module": "ESNext",
"moduleResolution": "nodenext",
"target": "ESNext",
...
does anybody have any ideas?
just wanted to use hotscript in my code but this "bug" currently stops me from using it.
If we want this to be used, we will need to setup a good documentation website. What are the best tools to build technical docs these days?
Examples:
Would this be in scope for this project?
Showing imports in the code sample allows users to quickly understand how to access hotscript
exports
It would be helpful to be able to specify Digits
and Digit
types.
import * as H from 'hotscript'
type Digits<T extends number> = H.Eval<H.Unions.Range<
H.Eval<H.N.Power<10, H.Eval<H.N.Sub<T, 1>>>>,
H.Eval<H.N.Sub<H.Eval<H.N.Power<10, T>>, 1>>
>> | (T extends 1 ? 0 : never);
type Digit = Digits<1>;
const some1DigitNumber: Digit = 5; // ✅
const some1DigitNumberString: `${Digit}` = "5"; // ✅
const some2DigitNumber: Digits<2> = 99; // ✅
const some2DigitNumberString: `${Digits<2>}` = "99"; // ✅
TypeScript Playground: https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAKjgQwM5wBJwGZQiOAcgAsIZUBjKYMGQg…
UPDATE: I've refined the example to include the recently supported Unions.Range
.
Attemnpting to make a route-function similar to the example shown in the readme, but simpler. Above code causes ts-error. Type instantiation is excessively deep and possibly infinite. ts(2589)
. Toggling Tuples.Zip
seems to trigger the error. In above example zip is utilised to transform ["user", "friend"]
to [["user"], ["friend"]]
and then mapping the latter to [["user", string], ["friend", string]]
and then ultimately to the final object. Thoughts?
I'm not sure about the root cause but maybe its about complexity of the type?
Playground Example of 1.0.11 (Works)
Playground Example of 1.0.12 (Doesn't work)
I've looked for some time, but didn't find a solution yet: is it possible to add some functions to get an AST of a type, or at least a text representation? At least basic ones... My idea is to be able to have a representation of a type in runtime, similar to JsonSchema but replaced inline. And if there's some way, can it be added to hotscript library?
Providing installation instructions allows users to understand how to install the package without thinking and it's always nice to get a break from thinking for a sec!
Reproduction:
type T1 = Call<Objects.Update<'a[0]', 4>, { a: 1 }>;
Output:
type T1 = {
a: {
toString: ((radix?: number | undefined) => string) & (() => string);
toFixed: (fractionDigits?: number | undefined) => string;
toExponential: (fractionDigits?: number | undefined) => string;
toPrecision: (precision?: number | undefined) => string;
valueOf: (() => number) & (() => Object);
toLocaleString: {
...;
} & (() => string);
0: number;
};
}
Expected:
type T1 = { a: number[] }
Hi, is this a bug?
type Bug<U extends "a" | "b" | "" = ""> = Call< Tuples.Map<Match<[
Match.With<"a", { a: true }>,
Match.With<"b", { b: true }>,
Match.With<any, Identity>,
]>>, Unions.ToTuple<U>>
type test1 = Bug<"a"> // every arg yields never
// but this half works
type HalfWorks<U extends "a" | "b" | "" = ""> = Call< Tuples.Map<Match<[
Match.With<"a", { a: true }>,
Match.With<"b", { b: true }>,
Match.With<any, Identity>,
]>>, [U]>
type test2 = HalfWorks<"a"> // [{ a: true; }]
type test3 = HalfWorks<"a" | "b"> // ["a" | "b"]
Did I miss anything?
Edit:
I realize there's a workaround:
Call< Unions.Map<Match<[
Match.With<"a", { a: true }>,
Match.With<"b", { b: true }>,
Match.With<any, Identity>,
]>>, U>
However, I'm curious why the above bug example doesn't work.
Wouldn't it be nice if we could write things like Strings.Replace<'([A-Z]_?[a-z])|([a-z]_?[A-Z])', '&1-&2'>
?
If somebody is up for a type challenge, here is a playground: Playground 😘
import {Pipe, S, T, Compose} from 'hotscript'
export type CamelCase<Str> = Pipe<
Str,
[
S.Replace<"/([a-z])([A-Z])/g", "$1-$2">,
S.Replace<"_", "-">,
S.Split<"-">,
T.Map<Compose<[S.Capitalize, S.Lowercase]>>,
T.Join<"">,
S.Uncapitalize
]
>;
type res1 = CamelCase<"ONE_two-three">;
// ^?
type test1 = Expect<Equal<res1, "oneTwoThree">>;
type res2 = CamelCase<"one_TWO-three">;
// ^?
type test2 = Expect<Equal<res2, "oneTwoThree">>;
type res3 = CamelCase<"one_two-THREE">;
// ^?
type test3 = Expect<Equal<res3, "oneTwoThree">>;
type res4 = CamelCase<"ONE_TWO-THREE">;
// ^?
type test4 = Expect<Equal<res4, "oneTwoThree">>;
type res5 = CamelCase<"alreadyInCamelCase">;
// ^?
type test5 = Expect<Equal<res5, "alreadyInCamelCase">>;
Hi, thank you for the library 👍
How about we add Object.Set
which is similar to Object.Update
. But you can update and set new paths.
Call<Objects.Set<'a.b', 2>, { a: 1 }>;
// { a: { b: 2 }}
U did use a POST request right?
Originally posted by @chukkyiii in dvf/blockchain#216 (comment)
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.