Reputation: 1443
Let's say I have this type code:
const shapes = {
circle: {
radius: 10
},
square: {
area: 50
}
}
type ShapeType = typeof shapes
type ShapeName = keyof ShapeType
type ParsedShape<NAME extends ShapeName, PROPS extends ShapeType[NAME]> = {
name: NAME,
properties: PROPS
}
Now, I want to use the key
of the shapes
object as the shape name when serialising. But after deserialisation I want to be able to figure out which shape it was. So I have this deserialisation code:
const parseShape = (json: string): ParsedShape<ShapeName, ShapeType[ShapeName]> => {
const parsed = JSON.parse(json)
return {
name: parsed.name,
properties: parsed.properties
}
}
The problem is - I am not able to discriminate the properties of the shape using name
:
const parsed = parseShape('{"name": "square", "properties": {"area": 50}}')
if (parsed.name === 'square') {
//ERROR
//Property area does not exist on type { radius: number; } | { area: number; }
//Property area does not exist on type { radius: number; }
console.log(parsed.properties.area)
}
So TypeScript is not seeing that I am actually checking for the shape name and is not narrowing down the properties.
Is there a way to achieve what I want or it is not possible?
An ugly workaround I am currently using is this and I would rather avoid that if possible:
type ParsedShape<NAME extends ShapeName> = {
[shapeName in NAME]?: ShapeType[shapeName]
}
const parseShape = (json: string): ParsedShape<ShapeName> => {
const parsed = JSON.parse(json)
return {
[parsed.name]: parsed.properties
}
}
const parsed = parseShape('{"name": "square", "properties": {"area": 50}}')
if (parsed.square) {
console.log(parsed.square.area)
}
Upvotes: 0
Views: 62
Reputation: 1443
Okay, so after a few answers I've got nudged into the right direction and found what I wanted. I took the code suggested by @0xts and asked GPT4 if it can be simplified, and that got me closer to the final result.
Basically, the "workaround" I was using was pretty close to the goal (notice the }[NAME]
at the end of the type declaration):
type ParsedShape<NAME extends ShapeName> = {
[shapeName in NAME]: {
name: shapeName
properties: ShapeType[shapeName]
}
}[NAME]
const parseShape = (json: string) => {
return JSON.parse(json) as ParsedShape<ShapeName>
}
const parsed = parseShape('{"name": "square", "properties": {"area": 50}}')
if (parsed.name === 'square') {
console.log(parsed.properties.area)
}
Thanks everyone!
Upvotes: 0
Reputation: 3029
Here's how you can achieve what you're trying to accomplish -
type Keys<T> = keyof T;
type DiscriminatedUnionOfRecord<
A,
B = {
[Key in keyof A as "_"]: {
[K in Key]: [
{ [S in K]: A[K] extends A[Exclude<K, Keys<A>>] ? never : A[K] }
];
};
}["_"]
> = Keys<A> extends Keys<B>
? B[Keys<A>] extends Array<any>
? B[Keys<A>][number]
: never
: never;
const shapes = {
circle: {
radius: 10
},
square: {
area: 50
}
};
type ShapeType = DiscriminatedUnionOfRecord<typeof shapes>;
type ShapeName = keyof typeof shapes;
type PraseShape<X extends ShapeName> = { [Obj in ShapeType as "_"]: { [Prop in keyof Obj as "_"]: X extends Prop ? { name: Prop, properties: Obj[Prop] } : never } }["_"]["_"];
const parseShape = (json: string): PraseShape<ShapeName> => {
const parsed = JSON.parse(json)
return {
name: parsed.name,
properties: parsed.properties
}
}
const parsed = parseShape('{"name": "square", "properties": {"area": 50}}');
if (parsed.name === 'square') {
console.log(parsed.properties.area)
// ^? (property) properties: { area: number; }
}
if (parsed.name === "circle") {
console.log(parsed.properties.radius);
// ^? (property) properties: { radius: number; }
}
Here's a Playground link. Let me know if I missed something in terms of requirements.
Upvotes: 0
Reputation: 4804
Probably it can be resolved by creating universal type guard.
UniversalShapeGuard
type takes a single type parameter representing the expected shape name. createShapeGuard
function then generates a type guard function that can be used to narrow down the type based on the specific shape name.
type UniversalShapeGuard<T extends ShapeName> = (obj: ParsedShape<ShapeName, any>) => obj is ParsedShape<T, ShapeType[T]>;
const shapeGuard = <T extends ShapeName>(name: T): UniversalShapeGuard<T> =>
(obj: ParsedShape<ShapeName, any>): obj is ParsedShape<T, ShapeType[T]> => obj.name === name;
const parsed = parseShape('{"name": "square", "properties": {"area": 50}}');
if (shapeGuard('circle')(parsed)) {
console.log(parsed.properties.radius)
}
Test is here
Upvotes: 0