Last active
March 1, 2022 01:25
-
-
Save jet2jet/5b06e87fd20d20a5dac93bf5b5965722 to your computer and use it in GitHub Desktop.
Addition and subtraction for numeric types in TypeScript 4.1 (https://www.pg-fl.jp/program/tips/ts_typecalc.htm (japanese))
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// for license: see https://gist.github.com/jet2jet/93fab0cbd08e89bf47f81835a2dfe46c | |
type NumericChars = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'; | |
type TupleLength<T extends any[]> = | |
any[] extends T ? never : T['length']; | |
// from microsoft/TypeScript#40336 | |
type Split<S extends string, D extends string> = | |
string extends S ? string[] : | |
S extends '' ? [] : | |
S extends `${infer T}${D}${infer U}` ? [T, ...Split<U, D>] : | |
[S]; | |
// used by MakeZeroTuple | |
interface MakeTuplesMap<T extends any[]> { | |
'0': []; | |
'1': [...T]; | |
'2': [...T, ...T]; | |
'3': [...T, ...T, ...T]; | |
'4': [...T, ...T, ...T, ...T]; | |
'5': [...T, ...T, ...T, ...T, ...T]; | |
'6': [...T, ...T, ...T, ...T, ...T, ...T]; | |
'7': [...T, ...T, ...T, ...T, ...T, ...T, ...T]; | |
'8': [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T]; | |
'9': [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T]; | |
// (extra field) | |
'10': [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T]; | |
} | |
/** | |
* Makes a tuple filled with '0' (T must be the tuple of NumericChars[]) | |
* @samples | |
* type Test1 = TupleLength<MakeZeroTuple<['2', '1', '0']>>; | |
* // --> 210 | |
*/ | |
type MakeZeroTuple<T extends any[]> = | |
T extends NumericChars[] | |
? NumericChars[] extends T | |
? [] | |
: T extends [] | |
? [] | |
: T extends [...infer U, infer V] | |
? V extends NumericChars | |
? [...MakeTuplesMap<MakeZeroTuple<U>>['10'], ...MakeTuplesMap<['0']>[V]] | |
: [] | |
: MakeTuplesMap<['0']>[T[0]] | |
: []; | |
type ConcatTuple<A extends any[], B extends any[]> = [...A, ...B]; | |
/** Check whether T is either a numeric literal or 'numeric string' literal */ | |
type CheckIsNumeric<T extends string | number | bigint> = | |
`${T}` extends `-${infer U}` | |
? U extends `-${any}` ? false : CheckIsNumeric<U> | |
: `${T}` extends `${NumericChars}${infer U}` | |
? U extends '' ? true : CheckIsNumeric<U> | |
: false; | |
type AddInner<A extends string[], B extends string[]> = | |
ConcatTuple<MakeZeroTuple<A>, MakeZeroTuple<B>> | |
type SubInner<A extends string[], B extends string[]> = | |
// store the result of MakeZeroTuple to RA | |
MakeZeroTuple<A> extends [...infer RA] | |
// store the result of MakeZeroTuple to RB | |
? MakeZeroTuple<B> extends [...infer RB] | |
? RA extends [...RB, ...infer R] | |
? TupleLength<R> | |
: RB extends [...RA, ...infer R] | |
? `-${TupleLength<R>}` | |
: never | |
: never | |
: never; | |
/** Makes a type of sum result for A and B */ | |
type Add<A extends string | number | bigint, B extends string | number | bigint> = | |
(CheckIsNumeric<A> & CheckIsNumeric<B>) extends true | |
? `${A}` extends `-${infer P}` | |
? `${B}` extends `-${infer Q}` | |
// use [...infer R] to suppress errors | |
// (`-${TupleLength<AddInner<...>>}` causes an error) | |
? AddInner<Split<P, ''>, Split<Q, ''>> extends [...infer R] ? `-${TupleLength<R>}` : never | |
: SubInner<Split<`${B}`, ''>, Split<P, ''>> | |
: `${B}` extends `-${infer Q}` | |
? SubInner<Split<`${A}`, ''>, Split<Q, ''>> | |
: TupleLength<AddInner<Split<`${A}`, ''>, Split<`${B}`, ''>>> | |
: never; | |
/** Makes a type of subtraction result for A and B */ | |
type Sub<A extends string | number | bigint, B extends string | number | bigint> = | |
(CheckIsNumeric<A> & CheckIsNumeric<B>) extends true | |
? B extends `-${infer Q}` | |
? Add<A, Q> | |
: Add<A, `-${B}`> | |
: never; | |
// Samples | |
type Sample1 = Add<123, 456>; // 579 | |
type Sample2 = Add<-10, 30>; // 20 | |
type Sample3 = Add<'1000', '-1000'>; // 0 | |
type Sample4 = Sub<700, 400>; // 300 | |
type Sample5 = Sub<12, 25> // '-13' (not -13); | |
type Sample6 = Add<10 | 20, 5>; // 15 | 25 | |
// idea is from microsoft/TypeScript#26362 | |
// (to support enum types) | |
type CheckIsNumericLiteralType<T extends number> = | |
true extends ({ [key: number]: true } & { [P in T]: false })[number] ? true : false | |
const enum Foo { | |
Bar = 1, | |
Baz = 2 | |
} | |
function add<A extends number, B extends number>( | |
a: (true extends CheckIsNumericLiteralType<A> & CheckIsNumeric<A> ? A : never), | |
b: (true extends CheckIsNumericLiteralType<B> & CheckIsNumeric<B> ? B : never) | |
): Add<A, B> extends infer R | |
? R extends number ? R : number // replace string literal types such as '-10' with number | |
: never; | |
function add(a: number, b: number): number; | |
function add(a: number, b: number): number { return a + b; } | |
const sample7 = add(10, 20); // type of sample7 is 30 | |
const sample8 = add(sample7, 30); // type of sample8 is 60 | |
const sample9 = add(Foo.Bar, Foo.Baz); // type of sample9 is 3 | |
const sample10 = add([].length, 100); // type of sample10 is number (type of [].length is not a numeric literal type) | |
const sample11 = add(10, -20); // UNSUPPORTED CASE: type of sample11 is number (not -10) | |
const sample12 = add(1.1, 2); // UNSUPPORTED CASE: type of sample12 is number |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment