Reputation: 107
I'm trying to map/match a class type to another type via a generics. An example will make that clearer:
class BaseClass {
//...
public someGenericClassStuff: 'someGenericClassStuff' = 'someGenericClassStuff';
}
class BaseProps {
//...
public someGenericStuff: 'someGenericStuff' = 'someGenericStuff';
}
class FooClass extends BaseClass {
//...
public someFooClassStuff: 'someFooClassStuff' = 'someFooClassStuff';
}
class FooProps extends BaseProps {
public someFooSpecificStuff: 'someFooSpecificStuff' = 'someFooSpecificStuff';
//...
}
class BarClass extends BaseClass{
//...
public someBarClassStuff: 'someBarClassStuff' = 'someBarClassStuff';
}
class BarProps extends BaseProps {
public someBarSpecificStuff: 'someBarSpecificStuff' = 'someBarSpecificStuff';
//...
}
type AllClass = BarClass | FooClass;
type AllProps = BarProps | FooProps;
type SpecificProps<T extends AllClass> = // Wondering what goes here
const test: SpecificProps<BarClass> = new BarProps(); // Works
const test2: SpecificProps<FooClass> = new FooProps(); // Works
const test3: SpecificProps<BarClass> = new FooProps(); // Fails
I tried a few options, here's what is getting me the closest:
type SpecificProps<T extends AllClass> =
T extends BarClass ? BarProps :
T extends FooClass ? FooProps :
never;
This solution works pretty decently in my example above, but when I start adding more and more classes and Unions between them it starts to fall apart and sometimes will return me wrong types. Also it's not the most convenient to write as I have a lot more than 2 classes and each time I add one I need to augment this type too.
My ideal solution would be to do something like
class BarClass extends BaseClass {
//...
public someBarClassStuff: 'someBarClassStuff' = 'someBarClassStuff';
type props = BarProps;
}
// And my type would become
type SpecificProps<T extends AllClass> = T['props'];
But I know this doesn't exist in typescript. I would have to create an actual variable instead of a type and it would take up memory for nothing other than the type matching.
Here's a TS Playground of the above example
Upvotes: 3
Views: 2541
Reputation: 403
Here's another example of a solution that works solely on the type level. It even works for arbitrary types and is not restricted to classes. (Playground link)
// ------------------------------------
// `Registry.ts`
interface Registry {}
type lookup<T> =
{
[k in keyof Registry]:
Registry[k] extends [T, infer P] ? P : never
}[keyof Registry]
// ------------------------------------
// `Class1.ts`
type Class1 = "C1"
type Props1 = "P1"
interface Registry {
someKey: [Class1, Props1]
}
// ------------------------------------
// `Class2.ts`
type Class2 = "C2"
type Props2 = "P2"
interface Registry {
someOtherKey: [Class2, Props2]
}
// ------------------------------------
// `SomeOtherPlaceInYourCode.ts`
// Correctly resolves to `"P1"`
type test1 = lookup<Class1>
// Correctly resolves to `"P2"`
type test2 = lookup<Class2>
// Correctly resolves to `never`
type test2 = lookup<"something else">
Upvotes: 1
Reputation: 330456
What you're looking for is something like "type families" for TypeScript, where you can associate types with other types, instead of having to drop down to the value level and create (or pretend to create) unnecessary-at-runtime properties to do so. There's an open GitHub request at microsoft/TypeScript#17588 asking for this, but I don't see much movement there.
You've already got a workaround involving building your own conditional type; there are probably tweaks to this which might behave better for your edge cases, but without a minimal, reproducible example of such edge cases I won't bother trying to go down that road.
Instead, let's look at a workaround that is similar to your desired type family solution. We create a "phantom" property value in each XXXClass
class, which doesn't actually exist at runtime, despite your claim that it does:
class BaseClass {
public someGenericClassStuff: 'someGenericClassStuff' = 'someGenericClassStuff';
declare __phantomPropsType: BaseProps;
}
Here I'm using the declare
property modifier. It has no affect on runtime, but now TypeScript thinks that every BaseClass
has a property named __phantomPropsType
. You can squint and read that as type propsType = BaseProps
if you want. Here's the rest of them:
class FooClass extends BaseClass {
public someFooClassStuff: 'someFooClassStuff' = 'someFooClassStuff';
declare __phantomPropsType: FooProps;
}
class BarClass extends BaseClass {
public someBarClassStuff: 'someBarClassStuff' = 'someBarClassStuff';
declare __phantomPropsType: BarProps;
}
And now your SpecificProps
falls out of it with a simple lookup, and no need to build a union called AllClass
:
type SpecificProps<T extends BaseClass> = T['__phantomPropsType']
This works the same as your example:
const test: SpecificProps<BarClass> = new BarProps(); // Works
const test2: SpecificProps<FooClass> = new FooProps(); // Works
const test3: SpecificProps<BarClass> = new FooProps(); // Fails
It's a workaround because the compiler really does think there's this __phantomPropsType
property. The double underscore is kind of a hint that it's a phantom property (you tend to see this in other usages like branded types), but it will still pop up "helpfully" in IntelliSense, etc:
const n = new FooClass();
n.__phantomPropsType // eh, not great
Upvotes: 3