Skip to content

Instantly share code, notes, and snippets.

@jet2jet

jet2jet/ts_typecalc.ts

Last active Oct 8, 2020
Embed
What would you like to do?
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
You can’t perform that action at this time.