Addition and subtraction for numeric types in TypeScript 4.8 (https://www.pg-fl.jp/program/tips/ts_typecalc2.htm (japanese))
This file contains 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 | |
// *** Must be used with TS 4.8 or later *** | |
type NumericChars = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'; | |
interface DigitToTuple { | |
'0': []; | |
'1': ['']; | |
'2': ['', '']; | |
'3': ['', '', '']; | |
'4': ['', '', '', '']; | |
'5': ['', '', '', '', '']; | |
'6': ['', '', '', '', '', '']; | |
'7': ['', '', '', '', '', '', '']; | |
'8': ['', '', '', '', '', '', '', '']; | |
'9': ['', '', '', '', '', '', '', '', '']; | |
'10': ['', '', '', '', '', '', '', '', '', '']; | |
'11': ['', '', '', '', '', '', '', '', '', '', '']; | |
'12': ['', '', '', '', '', '', '', '', '', '', '', '']; | |
'13': ['', '', '', '', '', '', '', '', '', '', '', '', '']; | |
'14': ['', '', '', '', '', '', '', '', '', '', '', '', '', '']; | |
'15': ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '']; | |
'16': ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']; | |
'17': ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']; | |
'18': ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']; | |
'19': ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']; | |
} | |
type IntTupleToIntString<T extends ['-' | '', ...NumericChars[]] | NumericChars[]> = | |
T extends [infer D extends NumericChars | '-' | '', ...infer R extends NumericChars[]] | |
? `${D}${IntTupleToIntString<R>}` | |
: T extends [] | |
? '' | |
: never; | |
type ParseIntStringToIntTupleInner<T extends string> = | |
string extends T | |
? never | |
: T extends `${infer P extends NumericChars}${infer Q extends string}` | |
? Q extends '' | |
? [P] | |
: [P, ...ParseIntStringToIntTupleInner<Q>] | |
: never; | |
type ParseIntStringToIntTuple<T extends string> = | |
string extends T | |
? never | |
: T extends `-${infer R}` | |
? ['-', ...ParseIntStringToIntTupleInner<R>] | |
: ['', ...ParseIntStringToIntTupleInner<T>]; | |
type AddXTemp<A extends NumericChars | '10', B extends NumericChars> = | |
[...DigitToTuple[A], ...DigitToTuple[B]]['length'] extends infer N extends number | |
? `${N}` | |
: never; | |
type Carried<D extends NumericChars, C extends '1' | '0'> = | |
C extends '1' | |
? [...DigitToTuple[D], '']['length'] extends infer N extends NumericChars | '10' | |
? `${N}` | |
: never | |
: D; | |
type AddInner<A extends NumericChars[], B extends NumericChars[], C extends '1' | '0' = '0'> = | |
A extends [] | |
? C extends '1' | |
? AddInner<B, ['1']> | |
: B | |
: B extends [] | |
? C extends '1' | |
? AddInner<A, ['1']> | |
: A | |
: A extends [...infer R extends NumericChars[], infer DA extends NumericChars] | |
? B extends [...infer S extends NumericChars[], infer DB extends NumericChars] | |
? AddXTemp<Carried<DA, C>, DB> extends infer G | |
? [ | |
...AddInner< | |
R, | |
S, | |
G extends `1${NumericChars}` | |
? '1' | |
: '0' | |
>, G extends `1${infer DG extends NumericChars}` ? DG : G | |
] | |
: never | |
: never | |
: never; | |
type SubXTemp<A extends NumericChars, B extends NumericChars, C extends '1' | '0'> = | |
C extends '1' | |
? DigitToTuple[A] extends [...DigitToTuple[B], '', ...infer R] | |
? ['0', `${R['length']}`] | |
: DigitToTuple[`1${A}`] extends [...DigitToTuple[B], '', ...infer R] | |
? ['1', `${R['length']}`] | |
: never | |
: DigitToTuple[A] extends [...DigitToTuple[B], ...infer R] | |
? ['0', `${R['length']}`] | |
: DigitToTuple[`1${A}`] extends [...DigitToTuple[B], ...infer R] | |
? ['1', `${R['length']}`] | |
: never; | |
type NumCharTupleToEmptyStrTuple<T extends NumericChars[]> = | |
T extends [NumericChars, ...infer R extends NumericChars[]] | |
? ['', ...NumCharTupleToEmptyStrTuple<R>] | |
: []; | |
// Must be A >= B | |
type SubInner2<A extends NumericChars[], B extends NumericChars[], C extends '1' | '0' = '0', HeadZero extends '0'[] = []> = | |
A extends [] | |
? B extends [] | |
? [''] | |
: never | |
: B extends [] | |
? C extends '1' | |
? [...SubInner2<A, ['1'], '0', HeadZero>] | |
: ['', ...A, ...HeadZero] | |
: A extends [...infer R extends NumericChars[], infer DA extends NumericChars] | |
? B extends [...infer S extends NumericChars[], infer DB extends NumericChars] | |
? SubXTemp<DA, DB, C> extends [infer F extends '1' | '0', infer D extends NumericChars] | |
? D extends '0' | |
? [...SubInner2<R, S, F, [D, ...HeadZero]>] | |
: [...SubInner2<R, S, F>, D, ...HeadZero] | |
: never | |
: never | |
: never; | |
type CompareNumTuple<A extends NumericChars[], B extends NumericChars[]> = | |
A extends [] | |
? B extends [] | |
? 0 | |
: -1 | |
: B extends [] | |
? 1 | |
: NumCharTupleToEmptyStrTuple<A> extends [...NumCharTupleToEmptyStrTuple<B>, '', ...''[]] | |
? 1 | |
: NumCharTupleToEmptyStrTuple<B> extends [...NumCharTupleToEmptyStrTuple<A>, '', ...''[]] | |
? -1 | |
: A extends [infer DA extends NumericChars, ...infer RA extends NumericChars[]] | |
? B extends [infer DB extends NumericChars, ...infer RB extends NumericChars[]] | |
? DigitToTuple[DA] extends [...DigitToTuple[DB], '', ...''[]] | |
? 1 | |
: DigitToTuple[DB] extends [...DigitToTuple[DA], '', ...''[]] | |
? -1 | |
: CompareNumTuple<RA, RB> | |
: never | |
: never; | |
type SubInner<A extends NumericChars[], B extends NumericChars[]> = | |
CompareNumTuple<A, B> extends infer C extends 1 | 0 | -1 | |
? C extends 1 | |
? SubInner2<A, B> | |
: C extends 0 | |
? ['', '0'] | |
: SubInner2<B, A> extends ['', ...infer R extends NumericChars[]] | |
? ['-', ...R] | |
: never | |
: never; | |
type AddImpl<A extends ['-' | '', ...NumericChars[]], B extends ['-' | '', ...NumericChars[]]> = | |
A extends [infer FA extends '-' | '', ...infer IA extends NumericChars[]] | |
? B extends [infer FB extends '-' | '', ...infer IB extends NumericChars[]] | |
? `${FA}${FB}` extends '--' | |
? ['-', ...AddInner<IA, IB>] | |
: `${FA}${FB}` extends '-' | |
? FA extends '-' | |
? SubInner<IB, IA> | |
: SubInner<IA, IB> | |
: ['', ...AddInner<IA, IB>] | |
: never | |
: never; | |
type AddIntString<A extends string, B extends string> = | |
IntTupleToIntString<AddImpl<ParseIntStringToIntTuple<A>, ParseIntStringToIntTuple<B>>>; | |
type ToggleFlag<A extends ['-' | '', ...NumericChars[]]> = | |
A extends ['-', ...infer R extends NumericChars[]] | |
? ['', ...R] | |
: A extends ['', ...infer R extends NumericChars[]] | |
? ['-', ...R] | |
: never; | |
type SubIntString<A extends string, B extends string> = | |
IntTupleToIntString< | |
AddImpl< | |
ParseIntStringToIntTuple<A>, | |
ToggleFlag<ParseIntStringToIntTuple<B>> | |
> | |
>; | |
type Add<A extends number, B extends number> = | |
AddIntString<`${A}`, `${B}`> extends `${infer N extends number}` | |
? N | |
: never; | |
type Sub<A extends number, B extends number> = | |
SubIntString<`${A}`, `${B}`> extends `${infer N extends number}` | |
? N | |
: never; | |
/** 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; | |
// 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 extends number | |
? R | |
: never; | |
function add(a: number, b: number): number; | |
function add(a: number, b: number): number { return a + b; } | |
// 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; | |
type Sample6 = Add<10 | 20, 5 | 15>; // 15 | 25 | 35 | |
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([1].length, 100); // type of sample10 is number (type of [1].length is not a numeric literal type) | |
const sample11 = add(([1] as const).length, 100); // type of sample11 is 101 (type of ([1] as const).length is 1) | |
const sample12 = add(10, -20); // type of sample12 is -10 | |
const sample13 = add(1.1, 2); // UNSUPPORTED CASE: type of sample13 is number |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment