Reputation: 19839
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
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
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