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
extendsplusinferladder in 060, and the deferred advanced tier of 112’s one-day syllabus. Thekeyofguard here mirrors thein-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.”

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">>,
];

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

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>}`;

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;

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

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

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.

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).