yuhr
yuhr

Reputation: 137

TypeScript recursive conditional mapped types

I need to test each value of multiple HTMLInputElements using a/some test function(s), and auto-generate a nested list of checkboxes for each, with the conditions which are expressed as Conditions below:

type TestFunction = (value: string) => boolean
type TestFunctionObject = { [description: string]: Conditions }
type Conditions = TestFunction | TestFunctionObject

So, for example, if I have:

const conds1: Conditions = {
  "Consists of alphanumerics": /^[a-zA-Z0-9]$/.test
}

I get a checkbox labeled as "Consists of alphanumerics". And if:

const conds2: Conditions = /^[a-zA-Z0-9]$/.test

I don't want a checkbox but just validate with that.

Auto-generating is done without any problem. Then, I wrote a type which represents validity of each TestFunction:

type Validity<C extends Conditions> = C extends TestFunction
  ? boolean
  : { [K in keyof C]: Validity<C[K]> }

Now I got an error from TS on C[K]; playground here. It says Type 'Conditions[K]' is not assignable to type 'TestFunctionObject'. Type conditioning doesn't seem to narrow Conditions to just TestFunctionObject.

How can I get it to work?

Addition for jcalz's answer: Playground with examples

Upvotes: 4

Views: 2177

Answers (1)

jcalz
jcalz

Reputation: 327994

I don't think the compiler does a lot of narrowing within the else clause of conditional types (after :) the way it does with values via control flow analysis. So while it's obvious to you that in the conditional type C extends TestFunction, the C in the else clause should extend TestFunctionObject, the compiler doesn't realize it.

But the compiler does do narrowing within the then clause (between ? and :), so the easiest fix for this is to add another conditional type:

type Validity<C extends Conditions> = C extends TestFunction ? boolean
  : C extends TestFunctionObject ? { [K in keyof C]: Validity<C[K]> } : never

Note that the last conditional type has never as the else clause. That's a common idiom with conditional types; sometimes you know the else clause cannot be reached; and in the absence of an invalid type, the never type is a good alternative.

Or, since you weren't doing much with the then clause to begin with, flip the clauses of your original check:

type Validity<C extends Conditions> = C extends TestFunctionObject ?
  { [K in keyof C]: Validity<C[K]> } : boolean

Either should work. Hope that helps; good luck!

Upvotes: 5

Related Questions