Home > OS >  Can Typescript infer keys of a generic type inside conditional blocks?
Can Typescript infer keys of a generic type inside conditional blocks?

Time:12-02

I've got a function that accepts a an enum value as T and a generic type Data<T> that chooses between two data types.

I hoped to be able to access properties of type BarData inside a conditional that should make T known. However, it still reads data as a union type.

The code works as expected but what do I have to change to get rid of the typescript errors?

enum DataType { Foo, Bar }
interface FooData { someKey: string }
interface BarData extends FooData { otherKey: string }

type Data<T extends DataType> = T extends DataType.Foo ? FooData : BarData

function func<T extends DataType>(type: T, data: Data<T>): void {
    const getter = <K extends keyof Data<T>>(key: K): Data<T>[K] => data[key]

    if (type === DataType.Bar) {
        data; // still inferred as Data<T>
        console.log(data.otherKey) // error Property 'otherKey' does not exist on type 'FooData | BarData'.
        console.log(getter('otherKey')) // error Argument of type 'string' is not assignable to parameter of type 'keyof Data<T>'.
    }
}

Playground link

CodePudding user response:

You need to make sure that invalid state is unrepresentable. You can use rest parameters instead of generic.

enum DataType { Foo = 'Foo', Bar = 'Bar' }

interface FooData { someKey: string }

interface BarData extends FooData { otherKey: string }


type MapStructure = {
  [DataType.Foo]: FooData,
  [DataType.Bar]: BarData
}

type Values<T> = T[keyof T]

type Tuple = {
  [Prop in keyof MapStructure]: [type: Prop, data: MapStructure[Prop]]
}

// ---- > BE AWARE THAT IT WORKS ONLY IN T.S. 4.6 < -----

function func(...params: Values<Tuple>): void {
  const [type, data] = params
  const getter = <Data, Key extends keyof Data>(val: Data, key: Key) => val[key]

  if (type === DataType.Bar) {
    const foo = type
    data; // BarData
    console.log(data.otherKey) // ok
    console.log(getter(data, 'otherKey')) // ok
    console.log(getter(data, 'someKey')) // ok

  }
}

Playground

MapStructure - is used just for mapping keys with valid state.

Values<Tuple> - creates a union of allowed tuples.Since rest parameters is nothing more than a tuple, it works like a charm.

Regarding getter. You should either define it inside if condition or make it separate function. SO, feel free to move getter out of the scope of func.

If you want to stick with generics, like in your original example, you should make type and data a part of one datastracture and then use typeguard

enum DataType { Foo, Bar }
interface FooData { someKey: string }
interface BarData extends FooData { otherKey: string }

type Data<T extends DataType> = T extends DataType.Foo ? FooData : BarData

const isBar = (obj: { type: DataType, data: Data<DataType> }): obj is { type: DataType.Bar, data: BarData } => {
    const { type, data } = obj;
    return type === DataType.Bar && 'other' in data
}

function func<T extends DataType>(obj: { type: T, data: Data<T> }): void {
    const getter = <K extends keyof Data<T>>(key: K): Data<T>[K] => obj.data[key]

    if (isBar(obj)) {
        obj.data // Data<T> & BarData
        console.log(obj.data.otherKey) // ok       
    }
}

But issue with getter still exists since it depend on uninfered obj.data. You either need to move out getter of func scope and provide extra argument for data or move getter inside conditional statement (not recommended).

However, you can switch to TypeScript nightly in TS playground and use object type for argument:

enum DataType { Foo, Bar }
interface FooData { someKey: string }
interface BarData extends FooData { otherKey: string }

type Data = { type: DataType.Foo, data: FooData } | { type: DataType.Bar, data: BarData }


function func(obj: Data): void {
    const { type, data } = obj;
    const getter = <K extends keyof typeof data>(key: K): typeof data[K] => data[key]

    if (type === DataType.Bar) {
        data // BarData
        console.log(obj.data.otherKey) // ok       
    }
}

Playground

getter still does not work in a way you expect, hence I recomment to move it out from func

  • Related