TS类型体操实战

166 阅读2分钟

是否听闻类型体操?类型体操在实际开发中有什么用处呢?本文带你粗略了解一下。体操主要在字符串转换那里,其他都是铺垫。

背景

vue项目中的路由定义

export default [
    {
        name: 'shapeAttrs',
        path: '/web/components/shape-attrs',
        component: () => import('@/views/components/shape-attrs.vue')
    },
{
        name: 'legend',
        path: '/web/components/legend',
        component: () => import('@/views/components/legend.vue')
    }
];

希望通过变量来使用路径,比如

<router-link :to="PATH.componentsShapeAttrs">绘图属性</router-link>

那么我们需要定义一个变量 PATH

const PATH = {
    componentsShapeAttrs: '/web/components/shape-attrs',
    componentsLegend: '/web/components/legend'
}

根据DRY原则,我们通过方法来生成PATH

import componetsRoute from './componets'
function transform(routeList: Route[]) {
    const result: Record<string, string> = {};
    routeList.forEach((route) => {
        const path = route.path;
        const camelKey = path
            .split('/')
            .slice(2)
            .map((item) => {
                return item
                    .split('-')
                    .map((item) => {
                        return item.charAt(0).toUpperCase() + item.slice(1);
                    })
                    .join('');
            })
            .join('');


        const key = camelKey.charAt(0).toLowerCase() + camelKey.slice(1);
        result[key] = path;
    });
    return result;
}
export const PATH = transform(route)

但是,这样我们PATH的类型就变成了

type Path = {
    [key: string]: string;
};

在使用时,就没有了代码提示。如何解决?

实现

1. 思路

首先确定我们期望的PATH的类型。

type Path = {
    componentsShapeAttrs: '/web/components/shape-attrs',
    componentsLegend: '/web/components/legend'
};

当然,跟期望的PATH的结构是一样的。

因此,我们也需要根据route定义,来生成PATH的类型。实现方式跟上面transform一样,只不过用类型体操实现一遍。

2.原理

实现之前,再看一遍路由原始定义

export default [
    {
        name: 'shapeAttrs',
        path: '/web/components/shape-attrs',
        component: () => import('@/views/components/shape-attrs.vue')
    },
{
        name: 'legend',
        path: '/web/components/legend',
        component: () => import('@/views/components/legend.vue')
    }
];

然后分析一下transform是如何实现的。

  1. 遍历 routerList

  2. 将path转化为key

    • 分隔字符串
    • 取后两位
    • 将每一项转为大驼峰
    • 将两个大驼峰字符串拼接
    • 转为小驼峰
  3. 将 { key: path }放到结果中

结构处理

首先,不考虑转换,如何生成type

const routeList = [
    {
        name: 'shapeAttrs',
        path: '/web/components/shape-attrs',
        component: '@/views/components/shape-attrs.vue'
    },
    {
        name: 'legend',
        path: '/web/components/legend',
        component: '@/views/components/legend.vue'
    }
] as const;


// 一、获取path列表
type KeyList = (typeof routeList)[number]['path'];
// type KeyList = "/web/components/shape-attrs" | "/web/components/legend"


// 二、遍历path列表,每一项作为结果的key与value
type Result = {
    [K in KeyList]: K;
};
// type Result = {
//     "/web/components/shape-attrs": "/web/components/shape-attrs";
//     "/web/components/legend": "/web/components/legend";
// }

字符串转换

const str = '/web/components/shape-attrs';


type Str = typeof str;


type Split<S extends string, Delimiter extends string> = string extends S
    ? string[]
    : S extends `${infer Start}${Delimiter}${infer Rest}`
    ? [Start, ...Split<Rest, Delimiter>]
    : [S];


// 一、分隔字符串
type strList = Split<Str, '/'>;
// type strList = ["", "web", "components", "shape-attrs"]


type LastTwoElements<T extends string[]> = T extends [...infer _Init, infer Penultimate, infer Last] ? [Penultimate, Last] : [];


// 二、获取最后两位元素
type lastTwo = LastTwoElements<strList>;
// type lastTwo = ["components", "shape-attrs"]


// 工具方法,将字符串转为大驼峰
type CamelCase<S extends string> = S extends `${infer First}-${infer Rest}` ? `${Capitalize<First>}${CamelCase<Rest>}` : Capitalize<S>;
type camelCase = CamelCase<lastTwo[1]>;
// type camelCase = "ShapeAttrs"


// 三、将两个字符串转为大驼峰 然后拼接
type CamelCaseJoin<S extends string[]> = S extends [infer First, ...infer Rest]
    ? First extends string
        ? Rest extends string[]
            ? `${CamelCase<First>}${CamelCaseJoin<Rest>}`
            : ''
        : ''
    : '';


type camelCaseJoin = CamelCaseJoin<lastTwo>;
// type camelCaseJoin = "ComponentsShapeAttrs"


// 四、首字母小写,变成小驼峰
type StrResult1 = Uncapitalize<camelCaseJoin>
// StrResult1 = "componentsShapeAttrs"


// 整合
type TransKey<T extends string> = Uncapitalize<CamelCaseJoin<LastTwoElements<Split<T, '/'>>>>;


type Result = TransKey<'/web/components/shape-attrs'>;
// type Result = "componentsShapeAttrs"

3.整合

type Split<S extends string, Delimiter extends string> = string extends S
    ? string[]
    : S extends `${infer Start}${Delimiter}${infer Rest}`
    ? [Start, ...Split<Rest, Delimiter>]
    : [S];


type CamelCase<S extends string> = S extends `${infer First}-${infer Rest}` ? `${Capitalize<First>}${CamelCase<Rest>}` : Capitalize<S>;


type LastTwoElements<T extends string[]> = T extends [...infer _Init, infer Penultimate, infer Last] ? [Penultimate, Last] : [];


type CamelCaseJoin<S extends string[]> = S extends [infer First, ...infer Rest]
    ? First extends string
        ? Rest extends string[]
            ? `${CamelCase<First>}${CamelCaseJoin<Rest>}`
            : ''
        : ''
    : '';


type TransKey<T extends string> = Uncapitalize<CamelCaseJoin<LastTwoElements<Split<T, '/'>>>>;


export type Route = {
    name: string;
    path: string;
    uri?: string;
    component: any;
};


export type Route2PathType<T extends readonly Route[]> = {
    [K in T[number]['path'] as TransKey<K>]: K;
};

成果展示