如何在Typescript中定义与正则表达式匹配的字符串类型?

143

是否有可能定义一个接口,其中包含关于字符串格式的一些信息? 以以下示例为例:

interface timeMarkers{
    markerTime: string[]        
};

一个例子是:

{
   markerTime: ["0:00","1:30", "1:48"]                   
}
我的问题是:是否有一种方法来定义markerTime的类型,使得字符串值必须始终与此正则表达式匹配,而不是将其声明为简单的string[],然后从那里开始? var reg = /[0-9]?[0-9]:[0-9][0-9]/;

5
无法定义这种类型。在GitHub上有一个支持此功能的建议,但目前似乎不是重点。我会搜索这个问题并在这里发布。 - Titian Cernicova-Dragomir
7
好的,以下是内容翻译:标题:TypeScript不能推断出某些类型参数正文:尝试将一个泛型函数返回类型设置为默认值,但 TypeScript 不能自动推断类型参数。这里有个简化的例子:function test(arg: T): T { return arg; } let x = test("test");在这个例子中,我期望 x 的类型应该是 string,但实际上 x 的类型是 unknown。当然,你可以明确指定类型参数来解决这个问题:let x = test("test");但这不是我的目标。我希望 TypeScript 能够自动推断出类型参数,就像在没有设置默认返回类型时一样。这个问题在 TypeScript 1.8 中就已经存在了,而且好像没有被解决。我不确定这是一个 bug 还是一个 feature,但无论如何,这种行为至少应该得到记录和说明。 - Titian Cernicova-Dragomir
@TitianCernicova-Dragomir 谢谢。如果您想将其作为答案,我会接受它。 - Our_Benefactors
无论将来是否需要,您仍然希望在生成的JavaScript输出中包含此内容。 - Steve Tomlin
有人知道已经实现这个的类型系统的例子吗?我做了一个快速搜索,只找到了一些研究论文和TS Github问题。其中一个Github评论说这将是“第一次”... - panepeter
9个回答

203

没有办法定义这种类型。在GitHub上有一个建议来支持这个,但目前似乎不是优先事项。投票并且也许团队会在未来的版本中包含它。

编辑

从4.1开始,你可以定义一个模板文字类型,它将验证字符串而无需实际定义所有选项:

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

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

游乐场链接


3
let x:MarkerTime = "99999:999999"; 由于${number}可以接受任何数字而不仅仅是一位数字,因此这也是有效的。 (注:这是一行代码,x被声明为MarkerTime类型并赋值为字符串"99999:999999",后面的句子解释了为什么这行代码有效。) - Nathan Adams
3
也可以匹配此模式的是:"93.242:942.23"。 - EyasSH
1
这个问题在 Github 上有一个新的线程:https://github.com/microsoft/TypeScript/issues/41160 - Valentin Vignal
2
你可以进一步避免匹配 "93.242:942.23"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
1
@Mouradif 请查看 https://www.typescriptlang.org/docs/handbook/utility-types.html#uppercasestringtype - ErikE
显示剩余5条评论

38

在正则表达式类型可用于此语言之前,你现在可以在TS 4.1中使用模板字面类型

让我拿问题的例子来说明如何建模一个名为Time的限时 string 类型。Time 期望以hh:mm格式的字符串(例如"23:59")进行传递,这里为了简化。

步骤1:定义HHMM类型

将以下代码粘贴到你的浏览器Web控制台中:

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 = ")
生成的结果,我们可以在TS中使用作为类型: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'

步骤二:声明Time

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

就是这么简单。

第三步:一些测试

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

这里有一个现成的代码示例,可以使用Time 进行操作。


1
这在小例子上运行得很好,但在更大的例子上会出现问题,因为TypeScript将生成所有可能组合的联合。 - Nathan Adams
@NathanAdams 是的,你可以有最多100,000个联合成员(https://github.com/microsoft/TypeScript/pull/40336),可以通过此播放示例进行验证(https://www.typescriptlang.org/play?#code/C4TwDgpgBAKgjABigXilOAfATBgzBgFgwFYMA2DAdgwA4MBODRAKFdEigGU4UoADACQBveAgC+w0RJGJpU4QFEAHgGMANgFcAJhAA8ogDToAfGL5QA9Baj0EdhM3bROWXoJnjJsr549zZ5lbo9nZGEABO4QD24axAA)。 - bela53
2
对于看到这个答案的任何人,第一部分不是代码的一部分,而是应该提前完成以获取所有组合作为字符串,然后可以将其用作常量字符串。当然,只有在您准备好在代码中拥有成千上万的选项时,这才是一个解决方案... - yoel halb
然而,这种复杂性是不必要的,可以参考我下面的答案。 - yoel halb

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

汇总了@bela53@yoel halb的思路,提供一个简洁的解决方案。

此解决方案为Time类型提供2039个枚举成员。

Ts Playground


${2}为20-23小时,难道不能简单地写成2吗?比如说:'2${D3}' ? - Drak

12
基于@bela53的答案,但更加简单明了,我们可以采用一个非常简单的解决方案,类似于@Titian的方法但没有缺点:
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

5
似乎“29:00”符合这种类型-所以这不是100%正确的?! - daniel-sc
修复那个问题并不难,@daniel-sc。 - SgtPooki
但这会使它变得更加复杂。在我看来,这使得@bela53的原始答案更简单。 - Dávid Leblay

2

警告:TypeScript的处理能力在@bela53的方法中存在限制...

基于@bela53答案,我尝试了以下用于IPv4地址的类型定义,结果产生了"TS2590: Expression produces a union type that is too complex to represent."错误。当我忽略TypeScript的错误并尝试构建时,这个定义导致IntelliJ消耗了大量的CPU时间(最终我不得不杀掉并重新启动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}`;

我不确定是否有任何解决方法。


4
这只是在强调一个问题,并不是对原问题的回答。这里没有提供解决方案。本来可以作为评论。 - Alex
这将导致2^32个联合成员,所以这可能是为什么它太复杂的原因。这是一个很好的例子,说明为什么正则表达式会很有用,但此时编译时类型检查的需求似乎相当小。更有可能的情况是这将是一个动态值,TypeScript无法检查,而且只需创建一个ValidIP类来接收和检查字符串或类似的内容就会更容易些。 - Arlen Beiler

2

对于MySQL日期/时间字符串

我尝试创建一个类型,反映MySQL日期时间字符串的值,例如 "2022-07-31 23:11:54"。

有趣的是,你几乎可以做到目前为止,但是如果你增加了任何更多的明确性,它最终要么变成任意类型,要么抱怨无法添加更多的类型。我认为它可以创建的类型数量是有限制的?

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

是的,你可以拥有的联合数量是有限制的。所以像这样的 '20.22-07-31 23:1.1:5004' 对于你的示例来说是可以的,这使得它有点无用。 - zoran404

1
只是为了补充@bela53的回答,可以使用const assertions来进行类型构建。
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}`

1

我现在也在寻找类似的功能!

然后我想到了这个问题:是否可以通过设置更复杂的开发环境来实现这个功能?也许你可以使用文件监视器来触发tsc并查找TypeError事件以更新*d.ts文件。

我的意思是:

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

作为一个钩子,这里有一个基本的建议:

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

也许这种解决方案可以让你在编译时根据正则表达式动态添加类型。你觉得呢?

0

我的两分钱


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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接