如何定义一个正则表达式匹配的字符串类型在类型?

有没有可能定义一个接口,其中包含有关字符串格式的一些信息?以下面这个例子为例:

interface timeMarkers{
markerTime: string[]
};

例如:

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

我的问题是: 有没有一种方法可以定义 markerTime的类型,这样字符串值就必须始终匹配这个正则表达式,而不是简单地声明为 string[]并从那里开始?

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

82514 次浏览

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 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

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?

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.

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

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.

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

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',
};


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}`

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', '24'
] 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}`