137

Is it possible to define an interface which has some information on the format of a string? Take the following example:

interface timeMarkers{
    markerTime: string[]        
};

an example would be:

{
   markerTime: ["0:00","1:30", "1:48"]                   
}

My question: Is there a way to define the type for markerTime such that that the string value must always match this regex, instead of declaring it as simply string[] and going from there?

var reg = /[0-9]?[0-9]:[0-9][0-9]/;

Our_Benefactors
  • 3,220
  • 3
  • 21
  • 27
  • 5
    There is no way to define such a type. There is a proposal on GitHub to support this, but it currently does not appear to be a priority. I'll search for the issue and post it here – Titian Cernicova-Dragomir Jul 20 '18 at 15:23
  • 7
    https://github.com/Microsoft/TypeScript/issues/6579 – Titian Cernicova-Dragomir Jul 20 '18 at 15:25
  • @TitianCernicova-Dragomir Thanks. If you want to put it as the answer, I will accept it – Our_Benefactors Jul 20 '18 at 15:33
  • Whether there will be in the future or not you would still want this in generated javascript output. – Steve Tomlin Oct 23 '20 at 08:03
  • Does anybody know examples of type systems already implementing this? Doing a quick search, I only found some research papers and the TS github issue. [One of the github comments](https://github.com/microsoft/TypeScript/issues/41160#issuecomment-991968338) says it would be "a first" … – panepeter Oct 31 '22 at 13:30

9 Answers9

195

There is no way to define such a type. There is a proposal on GitHub to support this, but it currently does not appear to be a priority. Vote on it and maybe the team might include it in a future release.

Edit

Starting in 4.1 you can define a template literal type that would validate the string without actually defining all the options:

type MarkerTime =`${number| ''}${number}:${number}${number}`

let a: MarkerTime = "0-00" // error
let b: MarkerTime = "0:00" // ok
let c: MarkerTime = "09:00" // ok

Playground Link

Yves M.
  • 29,855
  • 23
  • 108
  • 144
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • 3
    let x:MarkerTime = "99999:999999"; will also be valid as ${number} allows any number, not just one digit – Nathan Adams Jan 03 '21 at 17:05
  • 3
    Also `"93.242:942.23"` matches this. – EyasSH Jul 31 '21 at 20:53
  • 1
    There is a new thread on Github for this: https://github.com/microsoft/TypeScript/issues/41160 – Valentin Vignal Sep 09 '21 at 14:43
  • type `string` is the new `any`, and `number` will be next. Be explicit: If you don't want any number, don't use a type that allows any number. – SgtPooki Sep 21 '21 at 22:47
  • Thank you, this was the only useful post I could find after quite an unnecessary amount of searching. I just wanted a string that ends in (), jeez. – DexieTheSheep Jan 13 '22 at 19:38
  • I loved this solution but I am facing the following error: `TS1023: An index signature parameter type must be either 'string' or 'number'` can someone help me out? – R. Karlus Feb 28 '22 at 20:04
  • 2
    You can go further to avoid matching `"93.242:942.23"` : ```ts type SingleNumber = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; type MarkerTime =`${SingleNumber| ''}${SingleNumber}:${SingleNumber}${SingleNumber}`; ``` https://www.typescriptlang.org/play?#code/C4TwDgpgBAyglgOwOYBsIDkCuBbARhAJygF4oAGKAHygEYqoAmegZnoBZ6BWegNnoHZ6ADnoBOAFDjQkKAFkAhgQDWhACpxs0YgAMAJAG94yNFjyFqAcgsBfA0dQYc+AtYBcdxA9PPbhzyadCa21JNGAoeVc5RRUCdU0SKAAiMgBaMjIkqAB6bKhCAgB7AnEwqFwohWU1DS1kjNcMrNyoQqVSiHCAY0qYmoTSFNFGzJy8tskgA – Jalil Aug 04 '22 at 10:03
  • Thanks! Super cool solution for this problem I was also facing. Just wondering if I wanted to use the same approach @Jalil but for valid hex number characters and accept both cases is there a better way than this: `type HexChar = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | 'a' | 'A' | 'b' | 'B' | 'c' | 'C' | 'd' | 'D' | 'e' | 'E' | 'f' | 'F';` ? – Mouradif Oct 10 '22 at 15:39
  • 1
    @Mouradif Take a look at https://www.typescriptlang.org/docs/handbook/utility-types.html#uppercasestringtype – ErikE Nov 16 '22 at 21:42
  • 1
    Can this be extended to string of arbitrary length? For example, I want to have a string representing hexadecimal numbers, so 'ffee' is legit, but 'ffe' and 'hello' are not. – sinoTrinity Dec 12 '22 at 17:28
38

Until regex types become available to the language, you can now use template literal types in TS 4.1.

Let me refer to the question example and illustrate, how to model a time restricted string type called Time. Time expects strings in the format hh:mm (e.g. "23:59") here for simplification.

Step 1: define HH and MM types

Paste following code into your browser web console:
Array.from({length:24},(v,i)=> i).reduce((acc,cur)=> `${acc}${cur === 0 ? "" : "|"}'${String(cur).padStart(2, 0)}'`, "type HH = ")
Array.from({length:60},(v,i)=> i).reduce((acc,cur)=> `${acc}${cur === 0 ? "" : "|"}'${String(cur).padStart(2, 0)}'`, "type MM = ")
Generated result, which we can use as types in TS:
type HH = '00'|'01'|'02'|'03'|'04'|'05'|'06'|'07'|...|'22'|'23'
type MM = '00'|'01'|'02'|'03'|'04'|'05'|'06'|'07'|...|'58'|'59'

Step 2: Declare Time

type Time = `${HH}:${MM}`

Simple as that.

Step 3: Some testing

const validTimes: Time[] = ["00:00","01:30", "23:59", "16:30"]
const invalidTimes: Time[] = ["30:00", "23:60", "0:61"] // all emit error

Here is a live code example to get play around with Time.

bela53
  • 3,040
  • 12
  • 27
  • 1
    This works great on small examples, but breaks on larger ones as typescript will produce a union of all possible combinations – Nathan Adams Jan 03 '21 at 17:00
  • @NathanAdams yes, you can have a [maximum of 100.000 union constituents](https://github.com/microsoft/TypeScript/pull/40336), verifyable by [this playground sample](https://www.typescriptlang.org/play?#code/C4TwDgpgBAKgjABigXilOAfATBgzBgFgwFYMA2DAdgwA4MBODRAKFdEigGU4UoADACQBveAgC+w0RJGJpU4QFEAHgGMANgFcAJhAA8ogDToAfGL5QA9Baj0EdhM3bROWXoJnjJsr549zZ5lbo9nZGEABO4QD24axAA). – bela53 Jan 04 '21 at 12:33
  • 2
    To anyone seeing this answer, part one is NOT part of the code, rather should be done in advance to get all combinations as a string and this can then be used as constant strings, this is of course only a solution if you are ready to have thousands of options in your code... – yoel halb May 12 '21 at 21:22
  • However there is no need for this complexity, see my answer below. – yoel halb May 12 '21 at 21:24
22
type D1 = 0|1;
type D3 = D1|2|3;
type D5 = D3|4|5;
type D9 = D5|6|7|8|9;

type Hours = `${D9}` | `${D1}${D9}` | `2${D3}`;
type Minutes = `${D5}${D9}`;
type Time = `${Hours}:${Minutes}`;

Compact solution aggregating ideas from @bela53 and @yoel halb.

This solution has 2039 enum members for the Time type.

Ts Playground

Patiu Dan
  • 501
  • 1
  • 5
  • 7
12

Basing on the answer of @bela53 but much more simpler, we can do a very simple solution which is similar to what @Titian but without the drawbacks:

type HourPrefix = '0'|'1'|'2';
type MinutePrefix = HourPrefix | '3'|'4'|'5';
type Digit = MinutePrefix |'6'|'7'|'8'|'9';

type Time = `${HourPrefix | ''}${Digit}:${MinutePrefix}${Digit}`

const validTimes: Time[] = ["00:00","01:30", "23:59", "16:30"]
const invalidTimes: Time[] = ["30:00", "23:60", "0:61"] // all emit error
yoel halb
  • 12,188
  • 3
  • 57
  • 52
2

WARNING: There is a limit to what TypeScript can handle with @bela53 approach...

Based upon @bela53's answer I attempted the following type definition for an IPv4 address that results in "TS2590: Expression produces a union type that is too complex to represent." This definition caused IntelliJ to consume lots and lots of CPU time when I ignored the TypeScript error and tried to build anyway on the premise that it was still valid (I ended up needing to kill and restart IntelliJ).

type segment = '0'|'1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'|'10'|'11'|'12'|'13'|'14'|'15'|'16'|'17'|'18'|'19'|'20'|'21'|'22'|'23'|'24'|'25'|'26'|'27'|'28'|'29'|'30'|'31'|'32'|'33'|'34'|'35'|'36'|'37'|'38'|'39'|'40'|'41'|'42'|'43'|'44'|'45'|'46'|'47'|'48'|'49'|'50'|'51'|'52'|'53'|'54'|'55'|'56'|'57'|'58'|'59'|'60'|'61'|'62'|'63'|'64'|'65'|'66'|'67'|'68'|'69'|'70'|'71'|'72'|'73'|'74'|'75'|'76'|'77'|'78'|'79'|'80'|'81'|'82'|'83'|'84'|'85'|'86'|'87'|'88'|'89'|'90'|'91'|'92'|'93'|'94'|'95'|'96'|'97'|'98'|'99'|'100'|'101'|'102'|'103'|'104'|'105'|'106'|'107'|'108'|'109'|'110'|'111'|'112'|'113'|'114'|'115'|'116'|'117'|'118'|'119'|'120'|'121'|'122'|'123'|'124'|'125'|'126'|'127'|'128'|'129'|'130'|'131'|'132'|'133'|'134'|'135'|'136'|'137'|'138'|'139'|'140'|'141'|'142'|'143'|'144'|'145'|'146'|'147'|'148'|'149'|'150'|'151'|'152'|'153'|'154'|'155'|'156'|'157'|'158'|'159'|'160'|'161'|'162'|'163'|'164'|'165'|'166'|'167'|'168'|'169'|'170'|'171'|'172'|'173'|'174'|'175'|'176'|'177'|'178'|'179'|'180'|'181'|'182'|'183'|'184'|'185'|'186'|'187'|'188'|'189'|'190'|'191'|'192'|'193'|'194'|'195'|'196'|'197'|'198'|'199'|'200'|'201'|'202'|'203'|'204'|'205'|'206'|'207'|'208'|'209'|'210'|'211'|'212'|'213'|'214'|'215'|'216'|'217'|'218'|'219'|'220'|'221'|'222'|'223'|'224'|'225'|'226'|'227'|'228'|'229'|'230'|'231'|'232'|'233'|'234'|'235'|'236'|'237'|'238'|'239'|'240'|'241'|'242'|'243'|'244'|'245'|'246'|'247'|'248'|'249'|'250'|'251'|'252'|'253'|'254'|'255';
export type ipAddress = `${segment}.${segment}.${segment}.${segment}`;

I'm not sure if there is any workaround for this.

Neoheurist
  • 3,183
  • 6
  • 37
  • 55
  • 4
    This is only highlighting an issue, not an answer to the original question. No solution provided here. Could have been a comment – Alex Apr 09 '22 at 06:51
  • This would result in 2^32 union members, so that's probably why it's too complex. This is a good example of why regex would be useful, but at that point the need for compile time type checking seems pretty slim. It's far more likely that this is going to be a dynamic value, which typescript can't check anyway, and it would be a lot easier to just make a `ValidIP` class which receives and checks the string or something like that. – Arlen Beiler Feb 13 '23 at 14:16
2

For MySQL date/time strings

I was trying to make a type that reflected MySQL datetime string values ie "2022-07-31 23:11:54".

Interestingly, you can almost do it currently, but if you add any more specificity it will end up either being any or complain that it can't add more typing. I think there is limit to the # of typings it can create?

type OneToNine = 1|2|3|4|5|6|7|8|9
type ZeroToNine = 0|1|2|3|4|5|6|7|8|9

export type DateTimeType = `${
  `${number}`
}-${
  `0${OneToNine}` | `1${0|1|2}`
}-${
  `0${OneToNine}` | `1${ZeroToNine}` | `2${ZeroToNine}` | `3${0|1}`
} ${
  `0${OneToNine}` | `1${0|OneToNine}` | `2${0|1|2|3}`
}:${number}:${number}`
  • Yes, there is a limit to how many unions you can have. So something like this `'20.22-07-31 23:1.1:5004'` would be ok for your example, which makes it kind of useless. – zoran404 Sep 10 '22 at 11:16
1

I was just looking for a similar feature right now, too!

And I ended up thinking about this: Would'nt it be possible to get this running by setting up a little more complex dev-environment? Maybe you could use a file-watcher to trigger tsc and look up TypeError events to update your *d.ts file.

I mean something like:

export type superRegexType = 'type-1' | 'type-2' | '/type-/';

and as a hook something (rudimental suggestion):

const onTypeError = (err: Error, nameOfTypeSuperRegexType: string) => {
  const myTypesFile = require('fs').readFileSync(`path/to/\*d.ts`) as string;
  const searchFor = `export type ${nameOfTypeSuperRegexType} =`;
  const getDefinition = (inMyTypesFile: string, searchFor: string) => {
    const typeDefinitionString = inMyTypesFile.split(searchFor)[0].split(';')[0] + ';';
    const stringsInThere = typeDefinitionString.split(' | ').map(_str => _str.trim());
    const myRegexStr = stringsInThere.pop();
    return {
      oldTypeDefinitionString: typeDefinitionString,
      stringsInThere,
      myRegexStr,
      myRegex: new RegExp(myRegexStr)
    };
  };
  const myTypeDefinition = getDefinition(myTypesFile, searchFor);

  const shouldDynamicallyAddType = myTypeDefinition.myRegex.exec(err.message);
  if (!shouldDynamicallyAddType) {
    console.log("this is a real TypeError");
    console.error(err);
    return;
  } else {
    const indexInErrMessage = shouldDynamicallyAddType.index;
    const _endIdx = err.message.indexOf('\'');
    const typeToAdd = err.message.slice(indexInErrMessage, _endIdx).trim();
    myTypeDefinition.stringsInThere.push(typeToAdd);
    const updatedTypeDefinitionString = `${searchFor} ${myTypeDefinition.stringsInThere.join(' | ')} ${myTypeDefinition.myRegexStr};`;
    myTypesFile.replace(myTypeDefinition.oldTypeDefinitionString, updatedTypeDefinitionString);
    // --> save that new d.ts and return hopefully watch the lint-error disappearing
  }
}

Maybe this kind of solution would allow you to dynamically add types based on your RegEx on compiletime.

What do you think?

1

Just to complement @bela53's answer, the use const assertions can be used for the type construction.

const hours = [
  '00' , '01', '02', '03', '04', '05', '06', '07', '08',
  '09' , '10', '11', '12', '13', '14', '15', '16', 
  '17' , '18', '19', '20', '21', '22', '23'
] as const

type HH = typeof hours[number]

const minutes = [
  '00', '01', '02', '03', '04', '05', '06', '07', '08', '09', 
  '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', 
  '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', 
  '30', '31', '32', '33', '34', '35', '36', '37', '38', '39', 
  '40', '41', '42', '43', '44', '45', '46', '47', '48', '49', 
  '50', '51', '52', '53', '54', '55', '56', '57', '58', '59'
] as const 

type MM = typeof minutes[number]

type Time = `${HH}:${MM}`
Community
  • 1
  • 1
rustyBucketBay
  • 4,320
  • 3
  • 17
  • 47
0

My 2 cents


type digit01 = '0' | '1';
type digit03 = digit01 | '2' | '3';
type digit05 = digit03 | '4' | '5';
type digit09 = digit05 | '6' | '7' | '8' | '9';

type minutes = `${digit05}${digit09}`;
type hour = `${digit01 | ''}${digit09}` | `2${digit03}`;

type MarkerTime = `${hour}:${minutes}`;

const ok: Record<string, MarkerTime> = {
  a: '0:00',
  b: '09:00',
  c: '23:59',
};

const notOk: Record<string, MarkerTime> = {
  a: '0-00',
  b: '24:00',
  c: '93.242:942.23',
};