Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In TypeScript and/or JSDoc, how to indicate that some property names in a record type are aliases for sibling properties in the same type?

What's the right way to model an object property bag type (aka "record") in TS where some property names are alternate names (aka "aliases") for other properties? In particular we want to make VSCode aware of the aliasing so that it will only suggest (in IntelliSense autocomplete lists) the "main" property names and not the aliases, but we also don't want the TS compiler to fail if users manually type in aliases.

Here's more background: We're working on a .d.ts for a JS library which includes functions that accept time-like literals like {hour: 12, minute: 30, second: 0}. The library also accepts plural variants of these literals' properties e.g. {hours: 12, minutes: 30, seconds: 0}. Using the plural variants is not a best practice (and probably won't be documented beyond a quick note that they're allowed). But using these plural strings won't crash either.

What will crash is the case where the same unit is specified with both variants: e.g. {hour: 12, hours: 12}.

How should we model this type in TS?

Our goals:

  • Hide the "wrong" variants from IDE autocomplete. I assumed that I assume that the JSDoc ignore tag would be useful for this purpose, but it's not listed in the supported tags list in the TS docs and it doesn't seem to do anything in VSCode.
  • Allow the "wrong" variants without a TS compiler error.
  • Produce a TS compiler error if both singular and plural variants of the same property are included in the same object literal.
  • Ideally, also produce a compiler error if a smaller unit is present without the corresponding larger unit. For example, {hour: 10, second: 30} should produce an error. That said, I'm not sure this is possible in TS because of how union types work.
  • Ideally, a mix of the singular and plural variants should also produce a compiler error even if the property's alias is not present, e.g. {hours: 10, minute: 5}. It's also OK if this error is not flagged by TS because it's a rare mistake than the "2 variants of same unit" case noted above.
  • Ideally (this is optional because I suspect that it may not be possible), the plural variants would show an informational-level IDE warning (a light-blue squiggly line in VSCode) to encourage users to choose the correct variant instead of the wrong one, but without breaking compilation.

What's the recommended way to model these literals in TypeScript and/or JSDoc to achieve this desired behavior, or at least a subset of it?

Here's the plain type including both variants. This declaration doesn't exhibit any of the desired behavior above.

type Time = {
  hour?: number;
  minute?: number;
  second?: number;
  hours?: number;
  minutes?: number;
  seconds?: number;
};

JSDoc's @alias tag seems closest to our intent, but it doesn't seem to do anything in VSCode and it doesn't exempt the plural variants from autocomplete. I'm also unsure if @alias is intended to handle the case where the alias is pointing to a sibling member. All the examples in the JSDoc manual linked above are referring to cases where a member is defined with one name but used at runtime with another name, e.g. static class functions that are used with a class name prefix. I didn't see any examples where @alias was used to mirror another existing sibling property.

We could use JSDoc @deprecated but that implies that the values used to be OK but now aren't, which isn't quite true. But it does supply a message to alert users to use the other variant. This seems useful.

JSDoc's @ignore tag also seems like it might help, but it also doesn't seem to do anything in VSCode and it also doesn't exempt the plural variants from autocomplete.

like image 567
Justin Grant Avatar asked Nov 23 '25 18:11

Justin Grant


1 Answers

  • Hide the "wrong" variants from IDE autocomplete.
  • Allow the "wrong" variants without a TS compiler error.

The closest thing I can think for this is the new support for /** @deprecated */ in typescript 4.0.. This does not hide them from autocomplete, but it does mark them with a line through to discourage their use.

I do not believe it's possible to hide anything from autocomplete that is also a valid property according to the type system. The best you'll get is to mark for discouraged use.

yeah, it's not a perfect use of @deprecated, but if you want IDE level hinting, that's the best you'll get.

type Time = {
  hour?: number;
  minute?: number;
  second?: number;
  
  /** @deprecated */
  hours?: number;

  /** @deprecated */
  minutes?: number;

  /** @deprecated */
  seconds?: number;
};

enter image description here

enter image description here


For the rest, it doesn't sound like you want a single interface here. Instead you want a union of possible combinations. I think the only reasonable way to approach this is to create a type for each allowed combination.

But you also want to forbid some properties from some combinations, which you can do with something like { foo?: undefined }.

SO you can put something together like:

// Singular Components
type Hour = { hour: number }
type Minute = { minute: number }
type Second = { second: number }

type NoHour = { hour?: undefined }
type NoMinute = { minute?: undefined }
type NoSecond = { second?: undefined }
type NoSingular = NoHour & NoMinute & NoSecond

// Plural Components
type Hours = { hours: number }
type Minutes = { minutes: number }
type Seconds = { seconds: number }

type NoHours = { hours?: undefined }
type NoMinutes = { minutes?: undefined }
type NoSeconds = { seconds?: undefined }
type NoPlural = NoHours & NoMinutes & NoSeconds

// Singular time type
type SingularTime = NoPlural & (
  | (Hour & NoMinute & NoSecond)
  | (Hour & Minute & NoSecond)
  | (Hour & Minute & Second)
)

// Plural time type
type PluralTime = NoSingular & (
  | (Hours & NoMinutes & NoSeconds)
  | (Hours & Minutes & NoSeconds)
  | (Hours & Minutes & Seconds)
)

// Final time type
type Time = SingularTime | PluralTime

(I've omitted the /** @deprecated */ for brevity, but you'd have to add that in for all the time the plural property appears)

Which can be used like so:

// Good
const singular: Time = { hour: 1, minute: 2, second: 3 }
const plural: Time = { hours: 1, minutes: 2, seconds: 3 }

// Errors:
const samePropMix: Time = { hour: 1, hours: 2, minute: 3, second: 4 }
const diffPropMix: Time = { hour: 1, minutes: 2, second: 3 }
const missingMinutes: Time = { hour: 1, second: 3 }

Playground

like image 70
Alex Wayne Avatar answered Nov 26 '25 11:11

Alex Wayne