aleclofabbro
aleclofabbro

Reputation: 1699

Named Type association with Typescript

I have a type that associates names to types by mean of a Mapping Object Type TMap

it aims to feed a handler function with an association of one of the named types with a correspondent typed value

interface Thing<TMap extends { [k: string]: any }> {
  get<T extends keyof TMap>(handler: (v: TMap[T], t: T) => unknown): unknown
}

for example, say the Mapping Object type to be:

interface Types {
  num: number,
  dat: Date,
  str: string
}

and an instance of Things:

declare const thing: Thing<Types>

using the get method to get a couple of value, type works but loses type association when checking the type:

thing.get((v, t) => {
  // v: string | number | Date
  // t: "num" | "dat" | "str"
  if (t === 'num') {
    //  v: string | number | Date
    v
  } else if (t === 'dat') {
    //  v: string | number | Date
    v
  } else if (t === 'str') {
    //  v: string | number | Date
    v
  }
})

I managed a tricky workaround to fix that:

type Caster<TMap extends { [k: string]: any }> =
  <T extends keyof TMap>(
    v: TMap[keyof TMap],
    t: keyof TMap,
    isOfType: T
  ) => v is TMap[T]

declare const caster: Caster<Types>

thing.get((v, t) => {
  // v: string | number | Date
  // t: "num" | "dat" | "str"
  if (caster(v, t, 'num')) {
    //  v:  number 
    v
  } else if (caster(v, t, 'dat')) {
    //  v: Date
    v
  } else if (caster(v, t, 'str')) {
    //  v: string
    v
  }
})

How to properly declare Thing and Thing.get to keep type association avoiding tricky hacks?

[edit] check it out on TSPlayground

Upvotes: 3

Views: 1288

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249936

Type guards don't support narrowing the type of a different value then the one checked.

What we can do is change the callback to take in an object that is a union where a member of a union has the form { type: P, value T[P] } where T is the map type and P in turn every property of T

// Type that creates a union using the distributive property of union types.
type TypeUnion<T> = keyof T extends infer P ? // Introduce an extra type parameter P to distribute over
  P extends any ? { type: P, value: T[P] } :  // Take each P and create the union member
  never : never;

interface Thing<TMap extends { [k: string]: any }> {
  get(handler: (v: TypeUnion<TMap>) => unknown): unknown
}
declare const thing: Thing<Types>


interface Types {
  num: number,
  dat: Date,
  str: string
}

thing.get(o => { // o is  { type: "num"; value: number; } | { type: "dat"; value: Date; } | { type: "str"; value: string; }
  if (o.type === 'num') {
    o.value // number
  } else if (o.type === 'dat') {
    o.value // Date
  } else if (o.type === 'str') {
    o.value // string
  }
})

An alternative to conditional types for TypeUnion would be to use mapped type like this:

type TypeUnion<T> = {
  [P in keyof T] : { type: P, value: T[P]}
}[keyof T]

Upvotes: 3

Related Questions