Issue
I have a StringToString interface, which I use all over the place:
interface StringToString {
[key: string]: string;
}
I also exchange the key and values in my objects quite regularly. This means that the keys become values, and the values become keys. This is the type signature for the function.
function inverse(o: StringToString): StringToString;
Now here's the problem: Since this exchange is done quite often, I'd like to know if the object I'm treating has keys as values or keys as keys.
This means I'd like two types:
export interface KeyToValue {
[key: string]: string;
}
export interface ValueToKey {
[value: string]: string;
}
type StringToString = KeyToValue | ValueToKey;
With these, the inverse
function becomes:
/** Inverses a given object: keys become values and values become keys. */
export function inverse(obj: KeyToValue): ValueToKey;
export function inverse(obj: ValueToKey): KeyToValue;
export function inverse(obj: StringToString): StringToString {
// Implementation
}
Now I'd like typescript to show errors when I'm assigning to ValueToKey
to KeyToValue
. Which means I'd like this to throw an error:
function foo(obj: KeyToValue) { /* ... */ }
const bar: ValueToKey = { /* ... */ };
foo(bar) // <-- THIS SHOULD throw an error
Is that possible?
Solution
Branding can help here. It means you add a property that never really exists, except on the type, and use functions to cast values to the type with the correct brand.
Normally you might use a _brand: 'Something'
prop, but in this case you want to support all string keys, so you can use a Symbol
instead.
const brand = Symbol('KeysOrValuesBrand')
export interface KeyToValue {
[key: string]: string;
[brand]: 'KeyToValue'
}
export interface ValueToKey {
[value: string]: string;
[brand]: 'ValueToKey'
}
type StringToString = KeyToValue | ValueToKey
Now this works as you expect:
declare function foo(obj: KeyToValue): void
declare const bar: ValueToKey
foo(bar) // <-- THIS SHOULD throw an error
const valuesA: ValueToKey = bar // works
const valuesB: ValueToKey = inverse(bar) // error
const valuesC: ValueToKey = inverse(inverse(bar)) // works
The downside is that a function must make these objects for you, since that's how they get the brand. For example:
function makeValueToKey(input: Record<string, string>): ValueToKey {
return input as ValueToKey
}
function makeKeyToValue(input: Record<string, string>): KeyToValue {
return input as KeyToValue
}
declare function foo(obj: KeyToValue): void
foo({ a: 'b' }) // error
foo(makeValueToKey({ a: 'b' })) // error
foo(makeKeyToValue({ a: 'b' })) // works
Which may be kind of annoying.
That said, and this drifts into opinion, I believe that type branding is hack to hide a poor data model. If you can let the structural typing just work as intended, that's usually going be the better route.
It requires changing your data structures, but something like this is much simpler:
export interface KeyToValue {
type: 'KeyToValue'
data: Record<string, string>
}
export interface ValueToKey {
type: 'ValueToKey'
data: Record<string, string>
}
type StringToString = KeyToValue | ValueToKey
Answered By - Alex Wayne
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.