1

Link to minimum reproducible example: https://tsplay.dev/mL90eW

I have this data shape that I want to enforce in TypeScript to following requirements

  • Must have first field that want to call valueLabel (YearQtr here)
  • Must have at least one of other three fields that I want to call nameLabel (House, Unit, Land)
const data: [
  { YearQtr: '2000Q1', House: '100', Unit: '200', Land: '300' },
  { YearQtr: '2000Q2',             Unit: '400'                },
  { YearQtr: '2000Q4', House: '500',            Land: '600'   },
  { YearQtr: '2001Q2',             Unit: '700', Land: '800'   },

I have defined a type for it in TypeScript but this allows wrong entries in as well (see the last three rows in example below:

type StackbarDatum = {
    [key: string]: string
}

const data: StackbarDatum[] = [
  { YearQtr: '2000Q1', House: '100', Unit: '200', Land: '300' },
  { YearQtr: '2000Q2',             Unit: '400'                },
  { YearQtr: '2000Q4', House: '500',            Land: '600'   },
  { YearQtr: '2001Q2',             Unit: '700', Land: '800'   },
  { YearQtr: '2002Q1', House: '900', Unit: '1000'             },
  {                                                           }, // this should not be allowed
  { YearQtr: '2002Q1'                                         }, // this should not be allowed
  { House: '900', Unit: '1000'                                }, // this should not be allowed
]

I want to know how to do it properly in Type Safe way.

ranaalisaeed
  • 304
  • 3
  • 15

1 Answers1

0

Sounds like you want a union type like this:

type StackbarDatum = {
    YearQtr: string;
    House: string;
    Unit?: string;
    Land?: string;
} | {
    YearQtr: string;
    House?: string;
    Unit: string;
    Land?: string;
} | {
    YearQtr: string;
    House?: string;
    Unit?: string;
    Land: string;
}

Playground link


If you have a lot of properties that you want to force them to pick 1 or more of, then this union might get kinda combersome. In that case, you can do something similar by using mapped types. For example (inspired by this post):

type AtLeastOne<T> = { [K in keyof T]: Pick<T, K> }[keyof T]

type StackbarDatum = {
    YearQtr: string
} & AtLeastOne<{ 
    House: string;
    Unit: string;
    Land: string;
}>

Playground link


You talk about making this generic. If you mean you want a helper type to spit out other types that are in this format, then something like the following can work:

type Datum<ValueLabel extends string, NameLabel extends string> = { 
    [K in ValueLabel]: string
} & AtLeastOne<{
    [K in NameLabel]: string
}>

type StackbarDatum = Datum<'YearQtr', 'House' | 'Unit' | 'Land'>;
type OtherDatum = Datum<'Foo', 'Money' | 'Time' | 'Mojo'>;

Note that you still need to know the names of the properties at compile time, or typescript can't help.

Nicholas Tower
  • 72,740
  • 7
  • 86
  • 98
  • Nicholas Tower - is it possible to make the labels generic in this code? For example, for YearQtr, can I call it valueLabel? For House/Unit/Land, can I use generic property names? This way, as long as I get data in the same shape, my code should work – ranaalisaeed Apr 26 '21 at 03:54
  • When will you know the property names? Compile time, or runtime? – Nicholas Tower Apr 26 '21 at 04:03
  • I will know them at runtime as the users can pass in data (in the correct shape) and expect to see the output. – ranaalisaeed Apr 26 '21 at 04:06
  • `I will know them at runtime` Just so we're clear, i'm asking "when will you know that `"YearQtr"` is the required property". If that can't be known until runtime, then typescript can't really help. – Nicholas Tower Apr 26 '21 at 04:12
  • Allow me to clarify - this code will produce a 'data chart' in a re-usable way. So as long as I get one valueLabel ('YearQtr' in this example) and at least one nameLabel (one of 'House', 'Unit', or 'Land' in this example), my code should able to draw the chart. So a use may pass data like so `[{Quarter: Q1, Apples: 10, Oranges: 20}, {...}]` – ranaalisaeed Apr 26 '21 at 04:17
  • `{Quarter: Q1, Apples: 10, Oranges: 20}` How will your code know which one of these is the valueLabel and which ones are the nameLabel? – Nicholas Tower Apr 26 '21 at 04:19
  • As part of API documentation, I will publish that users should pass in at least one valueLabel (as the first property) and then any number of nameLabels. So my code should work on the assumption that first property is valueLabel and all remaining are nameLabels – ranaalisaeed Apr 26 '21 at 04:22
  • Pretty sure typescript can't help with that, sorry. – Nicholas Tower Apr 26 '21 at 04:25