10
\$\begingroup\$

What general tips do you have for golfing in TypeScript's type system? I'm looking for ideas which can be applied to code-golf problems and which are also at least somewhat specific to TypeScript's type system (e.g. "remove comments" is not an answer).

Please post one tip per answer.

If you're not familiar with programming in TypeScript's type system, the TypeScript Handbook and TypeScript Discord Server are both great resources, as well as the TypeScript Type System chatroom.

If you post any answers in TypeScript's type system, make sure to check out this deadlineless 50-100 rep bounty.

\$\endgroup\$
4
  • \$\begingroup\$ Cheating? tkdodo.eu/blog/calling-java-script-from-type-script (that is, give the answer as a .js file designed to be read by tsc). \$\endgroup\$
    – Joshua
    Commented Nov 29, 2021 at 4:23
  • \$\begingroup\$ I'm not sure that I understand your "remove comments" comment. Are you implying that comments don't count as golfed bytes? If so, use JSDoc for the types to get them for free. If not, can you clarify what you meant by that? \$\endgroup\$
    – jsejcksn
    Commented Nov 29, 2021 at 12:45
  • \$\begingroup\$ @jsejcksn That's pulled from the generic tips question; it means "don't say 'remove whitespace' or 'shorten identifiers', because that's applicable to all languages, not just TypeScript's type system" \$\endgroup\$
    – tjjfvi
    Commented Nov 29, 2021 at 13:16
  • 2
    \$\begingroup\$ @Joshua I'm not sure I understand. This is not about answers in TypeScript, but answers in TypeScript's type system, where the answer is a generic type that satisfies the challenge requirements. I'm not sure how calling out to JavaScript would be helpful for that. \$\endgroup\$
    – tjjfvi
    Commented Nov 29, 2021 at 13:20

12 Answers 12

9
\$\begingroup\$

Embrace the type errors

Type parameter constraints can often be replaced by a well-placed //@ts-ignore comment:

// 34 bytes
type Head<T extends any[]> = T[0];
// 33 bytes
//@ts-ignore
type Head<T> = T[0];
\$\endgroup\$
2
  • \$\begingroup\$ type Head<T> = T[0&keyof T]; here would be 28 bytes; of course you can also delete the semicolon and two spaces in any of these solutions. \$\endgroup\$
    – kaya3
    Commented Nov 29, 2021 at 15:10
  • \$\begingroup\$ @kaya3 Indeed, but the @ts-ignore trick can replace multiple constraints simultaneously \$\endgroup\$
    – tjjfvi
    Commented Nov 29, 2021 at 15:13
5
\$\begingroup\$

Use {} instead of any or unknown where applicable

The type {} counterintuitively means "anything that is not null or undefined, so it can be useful for shorter constraints, if you don't care about null/undefined:

// 34 bytes
type Head<T extends any[]> = T[0];
// 32 bytes
type Head<T extends{}[]> = T[0];
\$\endgroup\$
3
\$\begingroup\$

Use indexed access types as switch statements

Instead of

type Foo<T extends "a" | "b" | "c"> = 
    T extends "a"
    ? A
  : T extends "b"
    ? B
  : T extends "c"
    ? C
  : never

use

type Foo<T extends "a" | "b" | "c"> = {
  a: A,
  b: B,
  c: C,
}[T]

Note that in some cases this won't work for recursive types, as it might give the error 'foo' is referenced directly or indirectly in its own type annotation.

\$\endgroup\$
0
3
\$\begingroup\$

Use unions as sets

You can use T | U | V as a set, or [T] | [U] | [V] if you want to prevent merging (e.g. number | 1 becomes number).

Set operations:

  • Union: A | B
  • Intersection: A & B for sets of primitives, or Extract<A, B> more more complex types
  • Difference: Exclude<A, B>
  • Map: A extends A ? Foo<A> : 0 (if A is a type parameter)
  • Map: A extends infer B ? B extends A ? Foo<B> : 0 : 0 (if A is not a type parameter)
  • Map: A extends infer B ? [B]|[B] extends [A] ? Foo<B, A> (if A is a type parameter and you need to preserve the reference to the full set)
  • Empty set: never
  • Size:
//@ts-ignore
type CountUnion<T,N=[]>=(T extends T?(x:()=>T)=>0:0)extends(x:infer U)=>0?U extends()=>infer V?CountUnion<Exclude<T,V>,[...N,0]>:N:0;
\$\endgroup\$
1
  • \$\begingroup\$ If A is assignable to the keys if an object you can do {[K in A]:Foo<K>}[A] to map over it \$\endgroup\$ Commented Oct 4, 2023 at 20:26
3
\$\begingroup\$

Use any or shorter types for template literals

type EndsInPeriod<I> = I extends `${any}.` ? true : false;

Saving around 3 bytes (instead of the alternative ${string}.

\$\endgroup\$
2
\$\begingroup\$

Use type parameter defaults

Instead of

type RecursiveThing<T> = /* something recursive */
type Foo = RecursiveThing<Bar>;

Use

type Foo<T=Bar> = /* something recursive */

Instead of

type SomeConstant = /* something */;
type Foo<T> = /* something using that type */

Use

type Foo<T,SomeConstant = /*something*/>= /* something using that type */
\$\endgroup\$
1
\$\begingroup\$

Use tuples to do non-negative integer arithmetic

type IntToTuple<T, A extends any[] = []> = T extends A["length"] ? A : IntToTuple<T, [...A, 0]>;
type TupleToInt<T extends any[]> = T["length"];
type AddTuples<A extends any[], B extends any[]> = [...A, ...B];
type SubTuples<A extends any[], B extends any[]> = A extends [...B, ...infer C] ? C : never /* overflow */;
type MulTuples<A extends any[], B extends any[], C extends any[] = []> = A extends [0, ...infer A] ? MulTuples<A, B, [...C, ...B]> : C
type Add<A, B> = TupleToInt<AddTuples<IntToTuple<A>, IntToTuple<B>>>;
type Sub<A, B> = TupleToInt<SubTuples<IntToTuple<A>, IntToTuple<B>>>;
type Mul<A, B> = TupleToInt<MulTuples<IntToTuple<A>, IntToTuple<B>>>;

type Answer = Mul<Add<5, 2>, Sub<9, 3>>;
//   ^? - 42
\$\endgroup\$
1
\$\begingroup\$

Use stringification of number literal types to parse them

`${42}` becomes "42", which can be worked with more easily:

type Mod2<N extends number> = `${N}` extends `${string}${"1" | "3" | "5" | "7" | "9"}` ? 1 : 0;
\$\endgroup\$
2
  • \$\begingroup\$ Didn't even realize this was Mod2! A similar type was used & golfed in this even/odd challenge, but using an alternative array typing system instead. \$\endgroup\$
    – LeoDog896
    Commented May 3, 2023 at 14:22
  • \$\begingroup\$ Note that `${1.0}` becomes "1" \$\endgroup\$ Commented Oct 4, 2023 at 12:53
1
\$\begingroup\$

Use homomorphic mapped types to map tuples

type Foo<T> = [[[T]]];
type MapFoo<T> = {
  [K in keyof T]: Foo<T[K]>
}

type X = MapFoo<[1, 2, 3]>;
//   ^? - [Foo<1>, Foo<2>, Foo<3>]
\$\endgroup\$
1
\$\begingroup\$

Use this large collection of golfed utility types

\$\endgroup\$
1
\$\begingroup\$

Combine multiple extends clauses into one

If you have multiple extends clauses where you're just extracting part of a tuple/string/whatever and know that the match will succeed and don't care about the failure case, you can combine many clauses into one:

// Before:
type F<A,B>=A extends[infer D,...infer K]?B extends[5,6,infer F,...infer L]?[D,K,F,L]:0:0
// After:
type F<A,B>=[A,B]extends[[infer D,...infer K],[5,6,infer F,...infer L]]?[D,K,F,L]:0

In a case like this where you're just selecting part of each input, you can instead take the inputs as a tuple:

// Even better:
type F<A>=A extends[[infer D,...infer K],[5,6,infer F,...infer L]]?[D,K,F,L]:0

This example is purposely useless, but this can come in handy in real challenges. One use case is when zipping two tuples:

type F<A>=A extends[[infer X,...infer G],[infer Y,...infer H]]?[[X,Y],...F<A>]:A

I've used this technique in two submissions of mine: Add two really big numbers and Order by Earliest Lower Digit.

\$\endgroup\$
1
\$\begingroup\$

A couple minor byte-saves

Use A extends{length:N} instead of A["length"]extends N

This saves 1 byte off of decimal to unary conversion, among a few other things.

When taking the first element and the rest of a tuple, use {} and indexing instead of infer

This saves 2 bytes if you only use the inferred value once. For example, this

A extends[infer B,...infer C]?[B,C]:0

becomes

A extends[{},...infer C]?[A[0],C]:0

I will add more if I think of / encounter any.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.