#517Extreme

Multiply

Multiply two non-negative integers in the type system. Long multiplication over digit strings, built from a shift and repeated addition.

Type-level multiplication, one digit of the second operand at a time.

Multiply<A, B> multiplies two non-negative integers and returns the product as a string. Either side may arrive as a string, a number or a bigint, in any combination.

type T0 = Multiply<2, 3> // '6'
type T1 = Multiply<3, '5'> // '15'
type T2 = Multiply<'4', 10> // '40'
type T3 = Multiply<0, 16> // '0'
type T4 = Multiply<'13', '21'> // '273'
type T5 = Multiply<'43423', 321543n> // '13962361689'

This one continues from Sum, and the fastest route to a solution is to keep that addition machinery intact and build one layer on top of it. Nothing here counts to thirteen billion; the answer is assembled digit by digit, exactly as it is on paper.

Challenge Instructions: Multiply

Extreme

This challenge continues from 476 - Sum, it is recommended that you finish that one first, and modify your code based on it to start this challenge.*

Implement a type Multiply<A, B> that multiplies two non-negative integers and returns their product as a string. Numbers can be specified as string, number, or bigint.

For example,

type T0 = Multiply<2, 3> // '6'
type T1 = Multiply<3, '5'> // '15'
type T2 = Multiply<'4', 10> // '40'
type T3 = Multiply<0, 16> // '0'
type T4 = Multiply<'13', '21'> // '273'
type T5 = Multiply<'43423', 321543n> // '13962361689'

View on GitHub: https://tsch.js.org/517

Change the following code to make the test cases pass (no type check errors).

ChallengeSolution
/* _____________ Your Code Here _____________ */

type Multiply<
  A extends string | number | bigint,
  B extends string | number | bigint,
> = string

/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '../helpers'

type cases = [
  Expect<Equal<Multiply<2, 3>, '6'>>,
  Expect<Equal<Multiply<3, '5'>, '15'>>,
  Expect<Equal<Multiply<'4', 10>, '40'>>,
  Expect<Equal<Multiply<0, 16>, '0'>>,
  Expect<Equal<Multiply<'13', '21'>, '273'>>,
  Expect<Equal<Multiply<'43423', 321543n>, '13962361689'>>,
  Expect<Equal<Multiply<9999, 1>, '9999'>>,
  Expect<Equal<Multiply<4325

Pro Challenge

Unlock 150+ medium, hard, and extreme challenges to master advanced TypeScript.

Monthly subscription. Cancel anytime.

Detailed Explanation

The solution in full. Everything down to Add is the Sum challenge unchanged; the last four types are the new work:

type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
 
type Ones = {
  '0': []
  '1': [0]
  '2': [0, 0]
  '3': [0, 0, 0]
  '4': [0, 0, 0, 0]
  '5': [0, 0, 0, 0, 0]
  '6': [0, 0, 0, 0, 0, 0]
  '7': [0, 0, 0, 0, 0, 0, 0]
  '8': [0, 0, 0, 0, 0, 0, 0, 0]
  '9': [0, 0, 0, 0, 0, 0, 0, 0, 0]
}
 
type ColumnTotal<X extends Digit, Y extends Digit, C extends Digit> = [
  ...Ones[X],
  ...Ones[Y],
  ...Ones[C],
]['length']
 
type WrittenDown = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
                    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
 
type CarriedOver = ['0', '0', '0', '0', '0', '0', '0', '0', '0', '0',
                    '1', '1', '1', '1', '1', '1', '1', '1', '1', '1']
 
type ColumnIndex = Extract<keyof WrittenDown, number>
 
type Write<X extends Digit, Y extends Digit, C extends Digit> =
  ColumnTotal<X, Y, C> extends infer N extends ColumnIndex
    ? WrittenDown[N]
    : never
 
type Carry<X extends Digit, Y extends Digit, C extends Digit> =
  ColumnTotal<X, Y, C> extends infer N extends ColumnIndex
    ? CarriedOver[N]
    : never
 
type Reverse<S extends string> = S extends `${infer Head}${infer Rest}`
  ? `${Reverse<Rest>}${Head}`
  : ''
 
type AddColumns<
  A extends string,
  B extends string,
  C extends Digit,
  Acc extends string,
> = A extends `${infer X extends Digit}${infer ARest}`
  ? B extends `${infer Y extends Digit}${infer BRest}`
    ? AddColumns<ARest, BRest, Carry<X, Y, C>, `${Write<X, Y, C>}${Acc}`>
    : AddColumns<A, '0', C, Acc>
  : B extends ''
    ? C extends '1'
      ? `1${Acc}`
      : Acc
    : AddColumns<'0', B, C, Acc>
 
type Add<A extends string, B extends string> = AddColumns<
  Reverse<A>,
  Reverse<B>,
  '0',
  ''
>
 
type AddTimes<
  Acc extends string,
  A extends string,
  N extends unknown[],
> = N extends [unknown, ...infer Rest] ? AddTimes<Add<Acc, A>, A, Rest> : Acc
 
type ShiftLeft<S extends string> = S extends '0' ? '0' : `${S}0`
 
type MultiplyColumns<
  A extends string,
  B extends string,
  Acc extends string,
> = B extends `${infer D extends Digit}${infer BRest}`
  ? MultiplyColumns<A, BRest, AddTimes<ShiftLeft<Acc>, A, Ones[D]>>
  : Acc
 
type Multiply<
  A extends string | number | bigint,
  B extends string | number | bigint,
> = MultiplyColumns<`${A}`, `${B}`, '0'>

Everything down to Add is the Sum solution, wrapped so it takes two plain digit strings: it reverses both operands, walks them column by column, totals each column with tuple lengths and splits that total into a written digit and a carried digit through two twenty-element lookup tables. The Sum walkthrough takes it apart line by line. From here on it is a black box with one guarantee worth stating: given two strings without leading zeros, it returns a string without leading zeros.

Horner's method instead of partial products

School multiplication writes one partial product per digit of B, shifts each one, then adds the whole stack. That needs somewhere to keep the stack. The type system prefers a single accumulator, so the solution uses the other arrangement of the same arithmetic: start Acc at '0', read B from the left, and for each digit D set Acc to Acc * 10 + A * D. Every step multiplies what came before by ten and folds in one more digit, so after the last digit Acc is the product.

MultiplyColumns is that loop. The pattern `${infer D extends Digit}${infer BRest}` peels one digit off the front of B, the two adjacent infer placeholders making D match exactly one character while BRest takes the remainder. When B no longer matches, the accumulator is the answer.

Multiplying by ten, and by a single digit

Both operations inside the loop reduce to things that already exist. Multiplying by ten in decimal is appending a character, which is `${S}0`. The one case that needs care is '0', where appending would produce '00' and break the no-leading-zeros guarantee that Add relies on, so ShiftLeft guards it explicitly. Multiplying by a single digit is at most nine additions, so no digit-by-digit product table is needed at all.

AddTimes counts those additions down with a tuple. Ones[D] is already a tuple of length D, and N extends [unknown, ...infer Rest] pops one element per step, adding A to the accumulator each time; when the tuple is empty the recursion stops and the accumulator falls out unchanged. Ones is doing double duty here, as column arithmetic inside Add and as a loop counter outside it.

// Multiply<9, 99>
// D = '9'  shift '0' -> '0'      add '9' nine times   Acc = '81'
// D = '9'  shift '81' -> '810'   add '9' nine times   Acc = '891'
 
// Multiply<315, '100'>, where two of the three digits add nothing
// D = '1'  shift '0' -> '0'      add '315' once       Acc = '315'
// D = '0'  shift '315' -> '3150'                      Acc = '3150'
// D = '0'  shift '3150' -> '31500'                    Acc = '31500'

Three nested loops, each shrinking something, so termination is easy to see: MultiplyColumns removes one character from B per step, AddTimes removes one element from a tuple of length at most nine, and Add is bounded by the length of the longer operand. The total work is proportional to the digits of B times the digits of the product, never to the numbers themselves, which is why an eleven-digit answer costs about as much as a three-digit one.

Edge cases the tests cover

The whole solution is Sum plus a shift and a countdown. Once addition exists as a reusable type, multiplication is a loop around it.

This challenge is originally from here.

Share this challenge

Related Challenges

Learn the Concepts

Become a TypeScript Pro

Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.

Or start solving right away: explore all TypeScript challenges