Skip to content

Instantly share code, notes, and snippets.

@jet2jet
Last active March 1, 2022 01:25
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jet2jet/5b06e87fd20d20a5dac93bf5b5965722 to your computer and use it in GitHub Desktop.
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))
// 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