利用TypeScript从字符串字面量类型提取参数类型

目录
  • 正文
    • 挑战
    • 需要掌握的内容
    • 字符串字面量类型
    • 模板字面量类型和字符串字面量类型
    • 条件类型
    • 函数重载和通用函数
    • 着手解决问题
    • 分割字符串字面量类型
    • 参数语法部分的过滤
    • 在对象类型里做一个映射

正文

挑战

我们先来做一个ts的挑战。

你知道如何为下面的app.get方法定义TypeScript类型吗?

req.params是从传入的第一个参数字符串中提取出来的。

当你想对一个类似路由的函数定义一个类型时,这显得很有用,你可以传入一个带路径模式的路由,你可以使用自定义语法格式去定义动态参数片段(例如:[shopid]:shopid),以及一个回调函数,它的参数类型来源于你刚刚传入的路由。

所以,如果你尝试访问没有定义的参数,将会报错!

举一个真实案例,如果你对React Router很熟悉,应该知道render函数中的RouteProps的类型是从path参数派生出来的。

本文将探讨如何定义这样一个类型,通过各种ts技术,从字符串字面量类型中提取类型。

需要掌握的内容

首先,在我们探讨之前,需要先讲下一些基本的知识要求。

字符串字面量类型

ts的字符串类型是一个可以有任何值的字符串

let str: string = 'abc';
str = 'def'; // no errors, string type can have any value

而字符串字面量类型是一个具有特定值的字符串类型。

let str: 'abc' = 'abc';
str = 'def'; // Type '"def"' is not assignable to type '"abc"'.

通常情况下,我们将它与联合类型一起使用,用来确定你可以传递给函数、数组、对象的字符串取值的列表。

function eatSomething(food: 'sushi' | 'ramen') {}
eatSomething('sushi');
eatSomething('ramen');
eatSomething('pencil'); // Argument of type '"pencil"' is not assignable to parameter of type '"sushi" | "ramen"'.

let food: Array<'sushi' | 'ramen'> = ['sushi'];
food.push('pencil'); // Argument of type '"pencil"' is not assignable to parameter of type '"sushi" | "ramen"'.

let object: { food: 'sushi' | 'ramen' };
object = { food: 'sushi' };
object = { food: 'pencil' }; // Type '"pencil"' is not assignable to type '"sushi" | "ramen"'.

你是如何创建字符串字面量类型的呢?

当你使用const定义一个字符串变量时,它就是一个字符串字面量类型。然而,如果你用let去定义它,ts识别出变量的值可能会改变,所以它把变量分配给一个更通用的类型:

同样的情况对对象和数组也一样,你可以在以后去修改对象、数组的值,因此ts分配给了一个更通用的类型。

不过,你可以通过使用const断言向ts提示,你将只从对象、数组中读取值,而不会去改变它。

模板字面量类型和字符串字面量类型

从ts4.1开始,ts支持一种新的方式来定义新的字符串字面量类型,就是大家熟悉的字符串模板的语法:

const a = 'a';
const b = 'b';

// In JavaScript, you can build a new string
// with template literals
const c = `${a} ${b}`; // 'a b'

type A = 'a';
type B = 'b';

// In TypeScript, you can build a new string literal type
// with template literals too!

type C = `${A} ${B}`; // 'a b'

条件类型

条件类型允许你基于另一个类型来定义一个类型。在这个例子中,Collection<X>可以是number[]或者Set<number>,这取决于X的类型:

type Collection<X> = X extends 'arr' ? number[] : Set<number>;

type A = Collection<'arr'>; // number[]

// If you pass in something other than 'arr'
type B = Collection<'foo'>; // Set<number>

你使用extends关键字用来测试X的类型是否可以被分配给arr类型,并使用条件运算符(condition ? a : b)来确定测试成立的类型。

如果你想测试一个更复杂的类型,你可以使用infer关键字来推断该类型的一部分,并根据推断的部分定义一个新类型。

// Here you are testing whether X extends `() => ???`
// and let TypeScript to infer the `???` part
// TypeScript will define a new type called
// `Value` for the inferred type
type GetReturnValue<X> = X extends () => infer Value ? Value : never;

// Here we inferred that `Value` is type `string`
type A = GetReturnValue<() => string>;

// Here we inferred that `Value` is type `number`
type B = GetReturnValue<() => number>;

函数重载和通用函数

当你想在ts中定义一个参数类型和返回值类型相互依赖的函数类型时,可以使用函数重载或者通用函数。

function firstElement(arr) {
    return arr[0];
}
const string = firstElement(['a', 'b', 'c']);
const number = firstElement([1, 2, 3]);
// return string when passed string[]
function firstElement(arr: string[]): string;
// return number when passed number[]
function firstElement(arr: number[]): number;
// then the actual implementation
function firstElement(arr) {
    return arr[0];
}

const string = firstElement(['a', 'b', 'c']);
// Define type parameter `Item` and describe argument and return type in terms of `Item`
function firstElement<Item>(arr: Item[]): Item | undefined {
    return arr[0];
}

// `Item` can only be of `string` or `number`
function firstElement<Item extends string | number>(arr: Item[]): Item | undefined {
    return arr[0];
}

const number = firstElement([1, 3, 5]);
const obj = firstElement([{ a: 1 }]); // Type '{ a: number; }' is not assignable to type 'string | number'.

着手解决问题

了解了以上知识,我们对于问题的解决方案可能可以采取这样的形式:

function get<Path extends string>(path: Path, callback: CallbackFn<Path>): void {
	// impplementation
}

get('/docs/[chapter]/[section]/args/[...args]', (req) => {
	const { params } = req;
});

我们使用了一个类型参数Path(必须是一个字符串)。path参数的类型是Path,回调函数的类型是CallbackFn<Path>,而挑战的关键之处就是要弄清楚CallbackFn<Path>

我们计划是这样子的:

  • 给出path的类型是Path,是一个字符串字面量类型。
type Path = '/purchase/[shopid]/[itemid]/args/[...args]';
  • 我们派生出一个新的类型,这个类型将字符串分解成它的各个部分。
type Parts<Path> = 'purchase' | '[shopid]' | '[itemid]' | 'args' | '[...args]';
  • 筛选出只包含参数的部分
type FilteredParts<Path> = '[shopid]' | '[itemid]' | '[...args]';
  • 删除不需要的括号
type FilteredParts<Path> = 'shopid' | 'itemid' | '...args';
  • 将参数映射到一个对象类型中
type Params<Path> = {
	shopid: any;
	itemid: any;
	'...args': any;
};
  • 使用条件类型来定义map的值部分
type Params<Path> = {
	shopid: number;
	itemid: number;
	'...args': string[];
};
  • 重置键名,删除...args中的...
type Params<Path> = {
	shopid: number;
	itemid: number;
	args: string[];
};

最后

type CallbackFn<Path> = (req: { params: Params<Path> }) => void;

分割字符串字面量类型

为了分割一个字符串字面量类型,我们可以使用条件类型来检查字符串字面量的取值:

type Parts<Path> = Path extends `a/b` ? 'a' | 'b' : never;
type AB = Parts<'a/b'>; // type AB = "a" | "b"

但是要接收任意字符串字面量,我们无法提前知道是什么值

type CD = Parts<'c/d'>;
type EF = Parts<'e/f'>;

我们必须在条件测试中推断出数值,并使用推断出来的数值类型:

type Parts<Path> = Path extends `${infer PartA}/${infer PartB}` ? PartA | PartB : never;
type AB = Parts<'a/b'>; // type AB = "a" | "b"
type CD = Parts<'c/d'>; // type CD = "c" | "d"
type EFGH = Parts<'ef/gh'>; // type EFGH = "ef" | "gh"

而如果你传入一个不匹配模式的字符串字面量,我们希望直接返回:

type Parts<Path> = Path extends `${infer PartA}/${infer PartB}` ? PartA | PartB : Path;

type A = Parts<'a'>; // type A = "a"

有一点需要注意,PartA的推断是'non-greedily'的,即:它将尽可能地进行推断,但不包含一个/字符串。

type ABCD = Parts<'a/b/c/d'>; // type ABCD = "a" | "b/c/d"

因此,为了递归地分割Path字符串字面量,我们可以返回Parts<PathB>类型替代原有的PathB类型:

type Parts<Path> = Path extends `${infer PartA}/${infer PartB}` ? PartA | Parts<PartB> : Path;
type ABCD = Parts<'a/b/c/d'>; // type ABCD = "a" | "b" | "c" | "d"

以下是所发生的详细复盘:

type Parts<'a/b/c/d'> = 'a' | Parts<'b/c/d'>;
type Parts<'a/b/c/d'> = 'a' | 'b' | Parts<'c/d'>;
type Parts<'a/b/c/d'> = 'a' | 'b' | 'c' | Parts<'d'>;
type Parts<'a/b/c/d'> = 'a' | 'b' | 'c' | 'd';

参数语法部分的过滤

这一步的关键是观察到,任何类型与never类型联合都不会产生类型

type A = 'a' | never; // type A = "a"

type Obj = { a: 1 } | never; // type Obj = { a: 1; }

如果我们可以转换

'purchase' | '[shopid]' | '[itemid]' | 'args' | '[...args]'

never | '[shopid]' | '[itemid]' | never | '[...args]'

那我们就可以得到:

'[shopid]' | '[itemid]' | '[...args]'

所以,要怎么实现呢?

我们得再次向条件类型寻求帮助,我们可以有一个条件类型,如果它以[开始,以]结尾,则返回字符串字面量本身,如果不是,则返回never

type IsParameter<Part> = Part extends `[${infer Anything}]` ? Part : never;
type Purchase = IsParameter<'purchase'>; // type Purchase = never
type ShopId = IsParameter<'[shopid]'>; // type ShopId = "[shopid]"
type IsParameter<Part> = Part extends `[${infer Anything}]` ? Part : never;
type FilteredParts<Path> = Path extends `${infer PartA}/${infer PartB}` ? IsParameter<PartA> | FilteredParts<PartB> : IsParameter<Path>;
type Params = FilteredParts<'/purchase/[shopid]/[itemid]/args/[...args]'>; // type Params = "[shopid]" | "[itemid]" | "[...args]"

删除括号:

type IsParameter<Part> = Part extends `[${infer ParamName}]` ? ParamName : never;
type FilteredParts<Path> = Path extends `${infer PartA}/${infer PartB}` ? IsParameter<PartA> | FilteredParts<PartB> : IsParameter<Path>;
type ParamsWithoutBracket = FilteredParts<'/purchase/[shopid]/[itemid]/args/[...args]'>;

在对象类型里做一个映射

在这一步中,我们将使用上一步的结果作为键名来创建一个对象类型。

type Params<Keys extends string> = {
    [Key in Keys]: any;
};

const params: Params<'shopid' | 'itemid' | '...args'> = {
    shopid: 2,
    itemid: 3,
    '...args': 4,
};
type IsParameter<Part> = Part extends `[${infer ParamName}]` ? ParamName : never;
type FilteredParts<Path> = Path extends `${infer PartA}/${infer PartB}` ? IsParameter<PartA> | FilteredParts<PartB> : IsParameter<Path>;
type Params<Path> = {
    [Key in FilteredParts<Path>]: any;
};
type ParamObject = Params<'/purchase/[shopid]/[itemid]/args/[...args]'>;

最终版:

type IsParameter<Part> = Part extends `[${infer ParamName}]` ? ParamName : never;
type FilteredParts<Path> = Path extends `${infer PartA}/${infer PartB}` ? IsParameter<PartA> | FilteredParts<PartB> : IsParameter<Path>;
type ParamValue<Key> = Key extends `...${infer Anything}` ? string[] : number;
type RemovePrefixDots<Key> = Key extends `...${infer Name}` ? Name : Key;
type Params<Path> = {
    [Key in FilteredParts<Path> as RemovePrefixDots<Key>]: ParamValue<Key>;
};
type CallbackFn<Path> = (req: { params: Params<Path> }) => void;
function get<Path extends string>(path: Path, callback: CallbackFn<Path>) {
    // TODO: implement
}

到此这篇关于利用TypeScript从字符串字面量类型提取参数类型的文章就介绍到这了,更多相关TS取参数类型内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • TypeScript的类型指令单行注释详解

    目录 正文 @ts-ignore 和 @ts-expect-error @ts-check 和 @ts-nocheck 正文 单行注释应该在项目里用的很少吧, 我没见过在项目中使用过, 但是了解一下又不吃亏! 那么一起来看看吧!这里开启了TypeScript提示器. 这里谈谈我对它的理解,也可以看看林不渡的TypeScript小册 一般单行注释是以@ts-开头 @ts-ignore 和 @ts-expect-error @ts-ignore 和 @ts-expect-error 仅仅对紧随其后的

  • 详解Anyscript开发指南绕过typescript类型检查

    目录 前言 场景设定 解决方法 注释忽略 场景用例 类型断言 场景用例 泛型转换 场景用例 总结 前言 随着越来越多的前端项目采用 typescript 来开发,越来越多前端开发者会接触.使用这门语言.它是前端项目工程化的一个重要帮手,结合 vscode 编辑器,给予了前端开发者更严谨.高效的编码体验.但同时,严格的类型检查也会使部分开发者的编码效率有所降低,将时间花费在解决类型冲突.类型不匹配上,从而导致望而却步,迟迟不敢上手. 本文描述了几种绕过 typescript 类型检查的方法,帮助t

  • TypeScript实用技巧 Nominal Typing名义类型详解

    目录 Nominal Typing(名义类型) 概念解析 拓展应用 在Vue中的应用 Nominal Typing(名义类型) 概念解析 意思是给一个类型附加上一个“名义”,从而防止结构类型在某些情况下由于类型结构相似而被错用.假设有如下代码: interface Vector2D { x: number, y: number }; interface Vector3D { x: number, y: number, z: number }; function calc(vector: Vect

  • 利用TypeScript从字符串字面量类型提取参数类型

    目录 正文 挑战 需要掌握的内容 字符串字面量类型 模板字面量类型和字符串字面量类型 条件类型 函数重载和通用函数 着手解决问题 分割字符串字面量类型 参数语法部分的过滤 在对象类型里做一个映射 正文 挑战 我们先来做一个ts的挑战. 你知道如何为下面的app.get方法定义TypeScript类型吗? req.params是从传入的第一个参数字符串中提取出来的. 当你想对一个类似路由的函数定义一个类型时,这显得很有用,你可以传入一个带路径模式的路由,你可以使用自定义语法格式去定义动态参数片段(

  • JavaScript正则表达式匹配字符串字面量

    第一次遇到这个问题, 是大概两年前写代码高亮, 从当时的解决方案到现在一共有三代, 嘎嘎. 觉得还是算越来越好的. 第一代: //那个时候自己正则还不算很精通, 也没有(?:...)这种习惯, 是以寻找结束引号为入口写出的这个正则. 思路混乱, 也存在错误. //比如像字面量 "abc\\\"", 则会匹配为 "abc\\\", 而正确的结果应该是 "abc\\\"". var re = /('('|.*?([^\\]'|\\

  • js 正则学习小记之匹配字符串字面量优化篇

    昨天在<js 正则学习小记之匹配字符串字面量>谈到 /"(?:\\.|[^"])*"/ 是个不错的表达式,因为可以满足我们的要求,所以这个表达式可用,但不一定是最好的. 从性能上来说,他非常糟糕,为什么这么说呢,因为 传统型NFA引擎 遇到分支是从左往右匹配的, 所以它会用 \\. 去匹配每一个字符,发现不对后才用 [^"] 去匹配. 比如这样一个字符串: "123456\'78\"90" 共 16 个字符,除了第一个 &q

  • typescript返回值类型和参数类型的具体使用

    目录 返回值类型 可缺省和可推断的返回值类型 Generator 函数的返回值 参数类型 可选参数和默认参数 剩余参数 返回值类型 在 JavaScript 中,我们知道一个函数可以没有显式 return,此时函数的返回值应该是 undefined: function fn() { // TODO } console.log(fn()); // => undefined 需要注意的是,在 TypeScript 中,如果我们显式声明函数的返回值类型为 undfined,将会得到如下所示的错误提醒.

  • js正则学习小记之匹配字符串字面量

    今天看了第5章几个例子,有点收获,记录下来当作回顾也当作分享. 关于匹配字符串问题,有很多种类型,今天讨论 js 代码里的字符串匹配.(因为我想学完之后写个语法高亮练手,所以用js代码当作例子) var str1 = "我是字符串1哦,快把我取走", str2 = "我是字符串2哦,快把我取走"; 比如这样一个字符串,匹配起来很简单 /"[^"]*"/g 即可. PS: 白色截图是 chrome 34 控制台中运行的结果,深灰色是 su

  • 详解TypeScript映射类型和更好的字面量类型推断

    概述 TypeScript 2.1 引入了映射类型,这是对类型系统的一个强大的补充.本质上,映射类型允许w咱们通过映射属性类型从现有类型创建新类型.根据咱们指定的规则转换现有类型的每个属性.转换后的属性组成新的类型. 使用映射类型,可以捕获类型系统中类似Object.freeze()等方法的效果.冻结对象后,就不能再添加.更改或删除其中的属性.来看看如何在不使用映射类型的情况下在类型系统中对其进行编码: interface Point { x: number; y: number; } inte

  • javascript的数据类型、字面量、变量介绍

    数据类型: 1.数值型(整型int 浮点型floating) 2.字符串类型(string) 3.布尔型(只有两个值:ture fasle) 字符串字面量 1.转义系列: 在javascript中一些符号是辨别不出来的,只有转义之后正确显示出来.如: \' 单引号 \" 双引号 \n 换行符 \r 回车符 \\ 反斜杠 转义实例: 复制代码 代码如下: <script type="text/javascript"> document.write("hel

  • JavaScript 对象字面量讲解

    在编程语言中,字面量是一种表示值的记法.例如,"Hello, World!" 在许多语言中都表示一个字符串字面量(string literal ),JavaScript也不例外.以下也是JavaScript字面量的例子,如5.true.false和null,它们分别表示一个整数.两个布尔值和一个空对象. JavaScript还支持对象和数组字面量,允许使用一种简洁而可读的记法来创建数组和对象.考虑以下语句,其中创建了一个包含两个属性的对象(firstName和lastName): 还可

  • 一篇文章带你入门Java字面量和常量

    目录 引言 概念 字面量 字面量的分类 常量 总结 引言 ♀ 小AD:哥,前两天我没有闪现到刺客脸上了吧 ♂ 明世隐:在这方面做的有进步. ♀ 小AD:明哥教的好,通过学习Java关键字,游戏水平也得到了提升,一举两得,舒服. ♂ 明世隐:可是你看到残血还是上头啊,是了多少次,你说? ♀ 小AD:5.6次吧 ♂ 明世隐:岂止5.6,起码10次. ♀ 小AD:这不是看到200金币,经不住诱惑吗 ♂ 明世隐:关爱残血,你学哪里去了,游戏中就不能多一些人间的关爱吗?你就不能关爱一下放暑假的小弟弟小妹妹

  • 浅谈js之字面量、对象字面量的访问、关键字in的用法

    一:字面量含义 字面量表示如何表达这个值,一般除去表达式,给变量赋值时,等号右边都可以认为是字面量. 字面量分为字符串字面量(string literal ).数组字面量(array literal)和 对象字面量(object literal),另外还有函数字面量(function literal). 示例: var test="hello world!"; "hello world!"就是字符串字面量,test是变量名. 二:对象字面量 对象字面量有两种访问方式

随机推荐