带有推理的打字稿模板文字
Typescript template literals with inference
代码约定用“$”标记实体的子项(= 与另一个实体的关联)。
class Pet {
owner$: any;
}
在引用子实体时,应允许用户使用完整形式 ('owner$') 或更简单的形式 ('owner')。
我正在尝试这样的构造:
type ChildAttributeString = `${string}$`;
type ShortChildAttribute<E> = ((keyof E) extends `${infer Att}$` ? Att : never);
type ChildAttribute<E> = (keyof E & ChildAttributeString) | ShortChildAttribute<E>;
const att1: ChildAttribute<Pet> = 'owner$'; // Valid type matching
const att2: ChildAttribute<Pet> = 'owner'; // Valid type matching
const att3: ChildAttribute<Pet> = 'previousOwner$'; // Invalid: previousOwner$ is not an attribute of Pet - Good, this is expected
只要 Pet 的所有属性都是子属性,这就有效,但是一旦我们添加非子属性,匹配就会中断:
class Pet {
name: string;
owner$: any;
}
const att1: ChildAttribute<Pet> = 'owner$'; // Valid type matching
const att2: ChildAttribute<Pet> = 'owner'; // INVALID: Type 'string' is not assignable to type 'never'
// To be clear: ChildAttribute<Pet> should be able to have these values: 'owner', 'owner$'
// but not 'name' which is not a child (no child indication trailing '$')
使该工作正常进行的正确类型是什么?
---编辑
我不清楚预期结果和“实体子项”的定义,因此发布了答案,所以我编辑了问题以使其更清楚。
ChildAttribute
应该 return 所有允许值的并集。
type RemoveDollar<
T extends string,
Result extends string = ''
> =
(T extends `${infer Head}${infer Rest}`
? (Rest extends ''
? (Head extends '$' ? Result : `${Result}${Head}`) : RemoveDollar<Rest, `${Result}${Head}`>) : never
)
// owner
type Test = RemoveDollar<'owner$'>
据我了解,如果值带有 $
我们可以应用短 getter 但如果 属性 没有 $
就像 name
-我们不能将 name$
用作 getter.
如果我的假设是正确的,这个解决方案应该适合你:
interface Pet {
name: string;
owner$: any;
}
type RemoveDollar<
T extends string,
Result extends string = ''
> =
(T extends `${infer Head}${infer Rest}`
? (Rest extends ''
? (Head extends '$' ? Result : `${Result}${Head}`) : RemoveDollar<Rest, `${Result}${Head}`>) : never
)
// owner
type Test = RemoveDollar<'owner$'>
type WithDollar<T extends string> = T extends `${string}$` ? T : never
// owner$
type Test2 = WithDollar<keyof Pet>
type ChildAttribute<E> = keyof E extends string ? RemoveDollar<keyof E> | WithDollar<keyof E> : never
const att1: ChildAttribute<Pet> = 'owner$'; // Valid type matching
const att2: ChildAttribute<Pet> = 'owner'; // Valid type matching
const att3: ChildAttribute<Pet> = 'previousOwner$'; // Invalid: previousOwner$ is not an attribute of Pet - Good, this is expected
递归
type RemoveDollar<
T extends string,
Result extends string = ''
> =
(T extends `${infer Head}${infer Rest}`
? (Rest extends ''
? (Head extends '$' ? Result : `${Result}${Head}`) : RemoveDollar<Rest, `${Result}${Head}`>) : never
)
/**
* First cycle
*/
type Call = RemoveDollar<'foo$'>
type First<
T extends string,
Result extends string = ''
> =
// T extends `{f} {oo$}
(T extends `${infer Head}${infer Rest}`
? (Rest extends '' // Rest is not empty
// This branch is skipped on first iteration
? (Head extends '$' ? Result : `${Result}${Head}`)
// RemoveDollar<'oo$', ${''}${f}>
: RemoveDollar<Rest, `${Result}${Head}`>
) : never
)
/**
* Second cycle
*/
type Second<
T extends string,
// Result is f
Result extends string = ''
> =
// T extends `{o}$ {o$}
(T extends `${infer Head}${infer Rest}`
? (Rest extends '' // Rest is not empty
// This branch is skipped on second iteration
? (Head extends '$' ? Result : `${Result}${Head}`)
// RemoveDollar<'o$', ${'f'}${o}>
: RemoveDollar<Rest, `${Result}${Head}`>
) : never
)
/**
* Third cycle
*/
type Third<
T extends string,
// Result is fo
Result extends string = ''
> =
// T extends `{o} {$}
(T extends `${infer Head}${infer Rest}`
? (Rest extends '' // Rest is not empty, it is $
// This branch is skipped on third iteration
? (Head extends '$' ? Result : `${Result}${Head}`)
// RemoveDollar<'$', ${'fo'}${o}>
: RemoveDollar<Rest, `${Result}${Head}`>
) : never
)
/**
* Fourth cycle, the last one
*/
type Fourth<
T extends string,
// Result is foo
Result extends string = ''
> =
// T extends `${$} {''}
(T extends `${infer Head}${infer Rest}`
? (Rest extends '' // Rest is empty
// Head is $ foo
? (Head extends '$' ? Result : `${Result}${Head}`)
// This branch is skipped on last iteration
: RemoveDollar<Rest, `${Result}${Head}`>
) : never
)
这里我们映射键:如果键以 $
结尾,我们包括完整形式和简短形式,否则我们忽略它:
type ValuesOf<T> = T[keyof T]
type ChildAttribute<E> =
ValuesOf<{ [K in keyof E]: K extends `${infer Att}$` ? K | Att : never }>
interface Pet {
name: string
owner$: any
}
type PetAttr = ChildAttribute<Pet> // "owner$" | "owner"
代码约定用“$”标记实体的子项(= 与另一个实体的关联)。
class Pet {
owner$: any;
}
在引用子实体时,应允许用户使用完整形式 ('owner$') 或更简单的形式 ('owner')。
我正在尝试这样的构造:
type ChildAttributeString = `${string}$`;
type ShortChildAttribute<E> = ((keyof E) extends `${infer Att}$` ? Att : never);
type ChildAttribute<E> = (keyof E & ChildAttributeString) | ShortChildAttribute<E>;
const att1: ChildAttribute<Pet> = 'owner$'; // Valid type matching
const att2: ChildAttribute<Pet> = 'owner'; // Valid type matching
const att3: ChildAttribute<Pet> = 'previousOwner$'; // Invalid: previousOwner$ is not an attribute of Pet - Good, this is expected
只要 Pet 的所有属性都是子属性,这就有效,但是一旦我们添加非子属性,匹配就会中断:
class Pet {
name: string;
owner$: any;
}
const att1: ChildAttribute<Pet> = 'owner$'; // Valid type matching
const att2: ChildAttribute<Pet> = 'owner'; // INVALID: Type 'string' is not assignable to type 'never'
// To be clear: ChildAttribute<Pet> should be able to have these values: 'owner', 'owner$'
// but not 'name' which is not a child (no child indication trailing '$')
使该工作正常进行的正确类型是什么?
---编辑
我不清楚预期结果和“实体子项”的定义,因此发布了答案,所以我编辑了问题以使其更清楚。
ChildAttribute
应该 return 所有允许值的并集。
type RemoveDollar<
T extends string,
Result extends string = ''
> =
(T extends `${infer Head}${infer Rest}`
? (Rest extends ''
? (Head extends '$' ? Result : `${Result}${Head}`) : RemoveDollar<Rest, `${Result}${Head}`>) : never
)
// owner
type Test = RemoveDollar<'owner$'>
据我了解,如果值带有 $
我们可以应用短 getter 但如果 属性 没有 $
就像 name
-我们不能将 name$
用作 getter.
如果我的假设是正确的,这个解决方案应该适合你:
interface Pet {
name: string;
owner$: any;
}
type RemoveDollar<
T extends string,
Result extends string = ''
> =
(T extends `${infer Head}${infer Rest}`
? (Rest extends ''
? (Head extends '$' ? Result : `${Result}${Head}`) : RemoveDollar<Rest, `${Result}${Head}`>) : never
)
// owner
type Test = RemoveDollar<'owner$'>
type WithDollar<T extends string> = T extends `${string}$` ? T : never
// owner$
type Test2 = WithDollar<keyof Pet>
type ChildAttribute<E> = keyof E extends string ? RemoveDollar<keyof E> | WithDollar<keyof E> : never
const att1: ChildAttribute<Pet> = 'owner$'; // Valid type matching
const att2: ChildAttribute<Pet> = 'owner'; // Valid type matching
const att3: ChildAttribute<Pet> = 'previousOwner$'; // Invalid: previousOwner$ is not an attribute of Pet - Good, this is expected
递归
type RemoveDollar<
T extends string,
Result extends string = ''
> =
(T extends `${infer Head}${infer Rest}`
? (Rest extends ''
? (Head extends '$' ? Result : `${Result}${Head}`) : RemoveDollar<Rest, `${Result}${Head}`>) : never
)
/**
* First cycle
*/
type Call = RemoveDollar<'foo$'>
type First<
T extends string,
Result extends string = ''
> =
// T extends `{f} {oo$}
(T extends `${infer Head}${infer Rest}`
? (Rest extends '' // Rest is not empty
// This branch is skipped on first iteration
? (Head extends '$' ? Result : `${Result}${Head}`)
// RemoveDollar<'oo$', ${''}${f}>
: RemoveDollar<Rest, `${Result}${Head}`>
) : never
)
/**
* Second cycle
*/
type Second<
T extends string,
// Result is f
Result extends string = ''
> =
// T extends `{o}$ {o$}
(T extends `${infer Head}${infer Rest}`
? (Rest extends '' // Rest is not empty
// This branch is skipped on second iteration
? (Head extends '$' ? Result : `${Result}${Head}`)
// RemoveDollar<'o$', ${'f'}${o}>
: RemoveDollar<Rest, `${Result}${Head}`>
) : never
)
/**
* Third cycle
*/
type Third<
T extends string,
// Result is fo
Result extends string = ''
> =
// T extends `{o} {$}
(T extends `${infer Head}${infer Rest}`
? (Rest extends '' // Rest is not empty, it is $
// This branch is skipped on third iteration
? (Head extends '$' ? Result : `${Result}${Head}`)
// RemoveDollar<'$', ${'fo'}${o}>
: RemoveDollar<Rest, `${Result}${Head}`>
) : never
)
/**
* Fourth cycle, the last one
*/
type Fourth<
T extends string,
// Result is foo
Result extends string = ''
> =
// T extends `${$} {''}
(T extends `${infer Head}${infer Rest}`
? (Rest extends '' // Rest is empty
// Head is $ foo
? (Head extends '$' ? Result : `${Result}${Head}`)
// This branch is skipped on last iteration
: RemoveDollar<Rest, `${Result}${Head}`>
) : never
)
这里我们映射键:如果键以 $
结尾,我们包括完整形式和简短形式,否则我们忽略它:
type ValuesOf<T> = T[keyof T]
type ChildAttribute<E> =
ValuesOf<{ [K in keyof E]: K extends `${infer Att}$` ? K | Att : never }>
interface Pet {
name: string
owner$: any
}
type PetAttr = ChildAttribute<Pet> // "owner$" | "owner"