Chet
Chet

Reputation: 19839

Transforming a generic interface type in TypeScript

I'm trying to figure out how to create a function that accepts a generic type that is an object with keys and values and returns a type with the same interface but modifies the values of that object. It's a little hard to explain in plain English, so here's an example:

Here's a contrived class that has a generic type and wraps a value

class Wrap<A> {
    constructor(public value: A) {}
}

I want to create two functions wrap and unwrap that take an object and make sure all the values are wrapped or unwrapped. Here are the functions in vanilla js:

function wrap(obj) {
    const result = {}
    Object.keys(obj).forEach(key => {
        if (obj[key] instanceof Wrap) {
            result[key] = obj[key]
        } else {
            result[key] = new Wrap(obj[key])
        }
    })
    return result
}

function unwrap(obj) {
    const result = {}
    Object.keys(obj).forEach(key => {
        if (obj[key] instanceof Wrap) {
            result[key] = obj[key].value
        } else {
            result[key] = obj[key]
        }
    })
    return result
}

I'm trying to figure out how to create type signatures for these functions while maintaining as much type safety as possible. For example, if the input type is an interface, I want to output an interface as well:

interface Ex1Input {
    a: number
    b: string
}

interface Ex1Output {
    a: Wrap<number>
    b: Wrap<string>
}

interface Ex2Input {
    a: number
    b: Wrap<string>
}

interface Ex2Output {
    a: Wrap<number>
    b: Wrap<string>
}

I do not want to input and output an arbitrary {[key: string]: Wrap<any>} because I want to maintain the type safety of the interface.

Same with the unwrap function:

interface Ex3Input {
    a: Wrap<number>
    b: Wrap<string>
}

interface Ex3Output {
    a: number
    b: string
}

interface Ex4Input {
    a: number
    b: Wrap<string>
}

interface Ex4Output {
    a: number
    b: string
}

I've tried a few different things... This is maybe the closest I've gotten, but I feel like its still pretty far from working as I would like.

interface WrapInput {
    [key: string]: any
}

interface WrapOutput extends WrapInput {
    [key: string]: Wrap<WrapInput[keyof WrapInput]>
}

function wrap<T extends WrapInput>(obj: T): WrapOutput {
    const result: WrapOutput = {}
    Object.keys(obj).forEach(key => {
        if (obj[key] instanceof Wrap) {
            result[key] = obj[key]
        } else {
            result[key] = new Wrap(obj[key])
        }
    })
    return result
}

Any ideas? Is this possible? Just to reiterate -- I'm interested in a solution that is as typesafe as possible. So if I use wrap with Ex1Input, the autocomplete in my editor should show me that there are only two properties on the output and their specific types.

Upvotes: 3

Views: 2128

Answers (2)

kimamula
kimamula

Reputation: 12599

You can use mapped types (MaybeWrapped and Wrapped in the following example).

class Wrap<Value> {
  constructor(public value: Value) {}
}

type MaybeWrapped<T> = {
  [K in keyof T]: T[K] | Wrap<T[K]>;
};

type Wrapped<T> = {
  [K in keyof T]: Wrap<T[K]>;
};

function wrap<T>(arg: MaybeWrapped<T>): Wrapped<T> {
  // your implementation here
}

function unwrap<T>(arg: MaybeWrapped<T>): T {
  // your implementation here
}

declare const input: {
  a: Wrap<string>
  b: Date
};

const wrapped = wrap(input);
wrapped.a.value.length; // wrapped.a is infered as Wrap<string>
wrapped.b.value.getTime(); // wrapped.b is infered as Wrap<Date>

const unwrapped = unwrap(input);
unwrapped.a.length; // unwrapped.a is infered as string
unwrapped.b.getTime(); // unwrapped.b is infered as Date

Upvotes: 2

ismail codar
ismail codar

Reputation: 43

Could you try the following..

function wrap<T>(obj: T): T {
    const result: any = {}
    Object.keys(obj).forEach(key => {
        if ((obj as any)[key] instanceof Wrap) {
            result[key] = (obj as any)[key]
        } else {
            result[key] = new Wrap((obj as any)[key])
        }
    })
    return result
}


const data = { someData: 1 }
var test = wrap(data)
test.someData //  the autocomplete in my editor shows someData with intellisense

Upvotes: 0

Related Questions