TypeScript Survival Guide

Favorite typescript videos

110 — Advanced TypeScript Challenges (Exclude, KebabCase, Get)

Full companion narrative for Will I Survive These Advanced TypeScript Challenges — Kyle solves MyExclude, KebabCase, and a lodash-style Get live: distributive conditionals, infer string-splitting, tail recursion, and keyof-guarded path access

Video: 110 · Will I Survive These Advanced TypeScript Challenges? · watch · ~29:39 · Web Dev Simplified (Kyle)

Source: this is a written companion to the video. Part of the Favorite TypeScript Videos curated path, module 03 (advanced types) — the payoff for the extends plus infer ladder in 060, and the deferred advanced tier of 112’s one-day syllabus. The keyof guard here mirrors the in-operator narrowing in 105.

the one idea: three challenges, one conditional-type toolkit

Kyle screen-shares the type-challenges leaderboard — orange medium tags above the red hard-55 block — and solves three challenges live in VS Code against Expect/Equal test cases: an easy MyExclude reimplementation, a medium KebabCase string rewrite (including a tail-recursion detour that, he admits, “absolutely blew my mind”), and a hard lodash-style Get for dot-path object access. Every solution is probed with a scratch Test probe type hovered in the editor. He closes on his own i18n library’s ParseOptionType as proof these techniques ship. His framing lands early.

Kyle: “I have always struggled with advanced TypeScript features, so I decided to test my limits.”

type-challenges leaderboard with medium tags above the hard block

the challenge rig: question type, your-code slot, Equal plus Expect cases

Each challenge declares the required type shape, leaves a Your Code Here slot, and asserts with compile-error-as-failure cases; the warm-up (type X = string) shows green cases meaning the slot satisfies every Equal. Tag hints (union, built-in, template-literal) steer the approach.

import type { Equal, Expect } from "@type-challenges/utils";

// your-code slot for the easy challenge
type MyExclude<T, U> = T extends U ? never : T;

type cases = [
	Expect<Equal<MyExclude<"a" | "b" | "c", "a">, "b" | "c">>,
	Expect<Equal<MyExclude<"a" | "b" | "c", "a" | "b">, "c">>,
];

easy challenge slot with the passing Equal cases

extends is an if AND a loop: distributive conditionals filter unions member-wise

T extends U ? never : T with a naked T distributes: each union member is tested individually and results re-unioned, so excluded members map to never (which vanishes from unions) while survivors pass through as themselves. Inside the true/false branches T names the single member, not the whole union — the duality Kyle calls “the core of almost every single advanced TypeScript thing you write.”

// A->never, B->B, C->C, reunioned to "b" | "c"
type MyExclude<T, U> = T extends U ? never : T;

type T1 = MyExclude<"a" | "b" | "c", "a">; // "b" | "c"

splitting strings: double-infer in a template literal yields head plus rest

S extends `${infer A}${infer Rest}` binds A to the minimal first character and Rest to the remainder, giving character-by-character recursion with no loop construct. The trailing : S branch is the empty-string base case that terminates the recursion.

type First<S extends string> = S extends `${infer A}${infer Rest}`
	? [A, Rest] // "Foo" -> ["F", "oo"]: A minimal, Rest takes all
	: []; // empty string base case

infer head-plus-rest split with the probe hover

case testing with intrinsics, and the dash false positive

Testing A extends Uppercase<A> detects capitals, but dashes and emoji also satisfy Uppercase, producing stray dashes in probes like "foo-bar". Swapping the test to Lowercase<A> keeps identical behavior on letters while routing non-cased characters down the passthrough branch.

// A extends Uppercase<A> misfires on "-" and emoji;
// testing Lowercase<A> instead routes them to passthrough
type KebabStep<A extends string, Rest extends string> =
	A extends Lowercase<A> ? `${A}${KebabCase<Rest>}` : `-${Lowercase<A>}${KebabCase<Rest>}`;

KebabCase mid-solve with the Lowercase branch and test cases

the leading-dash problem: look at Rest instead of using an accumulator

A lowercase-first-character still gains a - prefix; the accumulator fix (IsFirst extends boolean = true) is rejected because callers can pass false and break the invariant. The accepted fix lowercases the head unconditionally and tests Rest extends Capitalize<Rest> — decide on the NEXT character via Capitalize/Uncapitalize — so the first character never earns a dash.

type KebabCase<S extends string> = S extends `${infer A}${infer Rest}`
	? Rest extends Capitalize<Rest>
		? `${Lowercase<A>}-${KebabCase<Uncapitalize<Rest>>}`
		: `${Lowercase<A>}${KebabCase<Uncapitalize<Rest>>}`
	: S;

type T2 = KebabCase<"FooBarBaz">; // "foo-bar-baz"

the empty-string edge and the tail-recursion wrapper

Single-character inputs leave Rest as "", which takes the wrong branch and emits a trailing dash; ordering the Uncapitalized check first routes "" correctly without nested and/or ternaries. Long inputs then hit “type instantiation is excessively deep” past ~40-50 recursion levels. The fix Kyle calls mind-blowing: hoist the recursion into a head-position wrapper — the recursive KebabCase call gated by extends infer STR — so each recursive call returns bare with nothing appended, then derive the decorated string from STR in a non-recursive extends step guarded by STR extends string — after which the previously-failing long string resolves.

// tail-recursive shape: recursive call returns bare, decoration
// happens in a separate non-recursive extends step
type KebabCase<S extends string> =
	KebabCaseInner<S> extends infer STR
		? STR extends string
			? STR // decorated further via Capitalize/Uncapitalize checks here
			: never
		: never;

tail-recursion wrapper with the long-string probe passing

hard challenge Get: keyof guard, dot-splitting, recursion over the object

Direct T[K] indexing fails the constraint check, and unknown-based probing misfires because every key extends unknown. The working structure: K extends keyof T ? T[K] : <dot-split and recurse>, where K extends `${infer K1}.${infer KRest}` splits the path and Get<T[K1], KRest> descends when K1 extends keyof T. The keyof pre-check doubles as the literal-key fast path, so keys containing dots ("foo.bar" as one key) must be tested before splitting. Simplification attempts (dropping the keyof guard) fail indexing constraints and get reverted.

type Get<T, K extends string> = K extends keyof T
	? T[K] // literal key fast path, incl. dotted literal keys
	: K extends `${infer K1}.${infer KRest}`
		? K1 extends keyof T
			? Get<T[K1], KRest> // descend one level
			: never
		: never;

type Data = { foo: { bar: { count: number } }; hello: "world" };
type T3 = Get<Data, "foo.bar.count">; // number
type T4 = Get<Data, "missing">; // never

Get implementation with dot-path probes resolving

arrays and nesting come free: numeric indices are keyof the array

Because arrays are objects, keyof includes numeric indices, so Get<{ r: [1, 2] }, "r.0"> and even mixed paths like "arr.0.bar" resolve with no array-specific code — the episode’s passing probes include these cases. Challenge scope excluded arrays, but the implementation covers them structurally.

type T5 = Get<{ r: [1, 2, 3] }, "r.1">; // 2

array-index probes passing with no array-specific code

ParseOptionType ships these techniques in a real library

The end-card browser tab shows his own i18n library source: ParseOptionType<ParamType extends string, ParamName extends string> branching on "number" into Intl.NumberFormatOptions maps and on "plural" into partial-record option bags. Proof-by-showcase that the challenge techniques ship in real library types — the conditional branches over string literals into mapped option bags replay distribution and keyof checks in production code.

i18n library ParseOptionType as production proof

Prev: 109 — Ditching Try/Catch for Result Tuples · Next: 111 — Branded ID Types

Watch after 060: that video teaches extends-as-if, infer extraction, and template-literal parsing as isolated rungs — this video climbs all three under test pressure, and the Get solution is the keyof loop from that video applied to path strings. The distributive-extends move here is the type-level twin of the value-level narrowing in 113, and the infer string-splitting generalizes the template-literal mechanics behind 111’s Brand<T, B extends string> name parameter. Per 112’s syllabus this is the deferred tier — generics, conditionals, infer — you return to after the basics compound.

Frame timestamps run ~00:00 (leaderboard) through ~29:00 (end-card).