#Type-level pattern match & type narrowing

1 messages · Page 1 of 1 (latest)

sleek flicker
#

On a lib I working on, I have a ton of really annoying to write / read / maintain type generics, like this one :

type ResolveFieldType<K extends FormField<any>> = K extends {
  options: _FieldOptions;
}
  ? ResolveOptionsField<K>
  : K['type'] extends 'checkbox'
    ? ResolveCheckboxField<K>
    : K['type'] extends 'object' | 'group'
      ? ResolveObjectField<K>
      : K['type'] extends 'array-list' | 'array-tabs'
        ? ResolveArrayField<K>
        : K['type'] extends 'upload'
          ? ResolveUploadField<K>
          : K extends { type: 'array-variant' }
            ? ResolveArrayVariantField<K>
            : ResolvePrimitiveField<K>;

I thought I could solve this by impelmenting a Match utility type that provides a better syntax for this, & came up with this :

type Match<T, Patterns extends (any[] | [any, any])[], Fallback = never> = 
  Patterns extends [infer First, ...infer Rest]
    ? First extends [infer NewSource, infer Condition, infer Result]
      ? [NewSource] extends [T]
        ? [NewSource] extends [Condition]
          ? Result
          : Match<T, Rest extends (any[] | [any, any])[] ? Rest : never, Fallback>
        : Match<T, Rest extends (any[] | [any, any])[] ? Rest : never, Fallback>
      : First extends [infer Condition, infer Result]
        ? [T] extends [Condition]
          ? Result
          : Match<T, Rest extends (any[] | [any, any])[] ? Rest : never, Fallback>
        : never
    : Fallback;
#

Can be used such as (dead-simple example here, but real cases are more complex obviously) :


type Input = { type: 't1', prop1: string } | { type: 't2', prop2: number } | { type: 't3' }

type GetResult<T extends Input> = Match<T, [ 
    [{ type: 't1' },  T['prop1']],
    [{ type: 't2' },  T['prop2']]
], null>


type Result1 = GetResult<{ type: 't1', prop1: string }>
//    ^?

type Result2 = GetResult<{ type: 't2', prop2: number }>
//    ^?

type Result3 = GetResult<{ type: 't3' }>
//    ^?
```ts 

The returned type is correct, but GetResult has an error when trying to access T['prop1'] or T['prop2'] since T isn't being narrowed down.
placid viperBOT
#
chronicstone#0

Preview:```ts
...
type Input = { type: 't1', prop1: string } | { type: 't2', prop2: number } | { type: 't3' }

type GetResult<T extends Input> = Match<T, [
[{ type: 't1' }, T['prop1']],
[{ type: 't2' }, T['prop2']]
], null>

type Result1 = GetResult<{ type: 't1', prop1: string }>
// ^?

type Result2 = GetResult<{ type: 't2', prop2: number }>
// ^?

type Result3 = GetResult<{ type: 't3' }>
// ^?```

sleek flicker
#

Is there a way I can somehow narrow T based on the 1st argument of each tuple

shadow sigil
#

Does const T extends Input work?

pine palm
#

seems like XY problem, you could just use an interface to map

pine palm
shadow sigil
pine palm
#

yeah, not sure why you're suggesting that, this is all type-level

sleek flicker
#

Yep, all type level, which is why I can't think of a way to solve this

pine palm
#

on mobile so this is gonna take a while and might have some mistakes

sleek flicker
#

Take your time don't worry

pine palm
#
interface FieldType<K extends ...> {
  checkbox: ResolveCheckboxField<K>
  object: ResolveObjectField<K>
  group: ResolveObjectField<K>
  // etc
}

type GetFieldType<K extends FormField<unknown>> = K extends { options: _FieldOptions } ? OptionsField : FieldType<K>[K["type"]]
```might not fully work for your usecase but i think you can get the gist of it
#

conditionals aren't everything, so don't overcomplicate your types with them

#

your Match type is... tbh, pretty overkill, and i think it'd be more effort than it's worth tbh, since it's really complicated and would have its own maintenance cost

sleek flicker
#

That's actually a good point

#

Didn't think I could be doing it like this

#

Thanks a lot, I'm going to try something around this idea

pine palm
#

generally interfaces used as maps wouldn't be generic, but higher kinded types (passing around generic types without instantiation) don't exist in ts, and i don't know what exactly your other resolvers are doing

sleek flicker
#

Yeah that's exactly what I was observing. I think that's one of the reason Generics are harder to grasp than above programming concept when you first see them. I'm starting to get the hang of it, but I rely a bit too much on ternaries right now

#

These resolver each infer the expected type from a form field from a form schema.

Allows to define forms with 0 ui config, through schemas, & get full type inferrence out of the box :