打字稿字符串自动完成对象结构中途
Typescript string autocomplete object structure midway
我正在尝试为 next-intl 包中的函数 useTranslations
添加类型支持。
让我们假设我的 ./locales/en.ts
文件看起来像这样
const dict = {
one: {
two: {
three: "3",
foo: "bar"
}
}
}
export default dict
useTranslations
函数接受命名空间的可选参数。它 returns 另一个函数,调用时具有必需的命名空间参数。通过设置可选参数,它减少了将命名空间的那部分应用到所需参数的需要。这是一个例子:
const t1 = useTranslations() // no optional arg
const val1 = t("one.two.three") // 3
const val2 = t("one.two.foo") // bar
const t2 = useTranslations("one.two")
const val3 = t("three") // 3
const val4 = t("foo") // bar
我已经设法让类型工作以创建对象的字符串表示形式,但我在第一个参数继续到第二个时遇到问题。
到目前为止,这是我的代码
import dict from "./locales/en"
// `T` is the dictionary, `S` is the next string part of the object property path
// If `S` does not match dict shape, return its next expected properties
type DeepKeys<T, S extends string> = T extends object
? S extends `${infer I1}.${infer I2}`
? I1 extends keyof T
? `${I1}.${DeepKeys<T[I1], I2>}`
: keyof T & string
: S extends keyof T
? `${S}`
: keyof T & string
: ""
interface UseTranslationsReturn<D> {
<S extends string>(
namespace: DeepKeys<D, S>,
values?: Record<
string,
| string
| number
| boolean
| Date
| ((children: ReactNode) => ReactNode)
| null
| undefined
>
): string
}
interface UseTranslationsProps<D = typeof dict> {
<S extends string>(namespace?: DeepKeys<D, S>): UseTranslationsReturn<D>
}
export const useTranslations: UseTranslationsProps = (namespace) => useNextTranslations(namespace)
我可以像这样设置主要功能,
const t = useTranslations("one.two")
但是我得到一个错误,
const val = t("three") // Argument of type '"three"' is not assignable to parameter of type "one"
如何调整类型来解决这个问题?
首先,useTranslations
应该期望有效的路径点符号。我的意思是它应该期望 "one" | "one.two" | "one.two.three" | "one.two.foo"
的并集。你会在评论中找到解释:
const dict = {
one: {
two: {
three: "3",
foo: "bar"
}
}
} as const
type Dict = typeof dict
type KeysUnion<T, Cache extends string = ''> =
/**
* If T is no more an object, it means that this call
* is last in a recursion, hence we need to return
* our Cache (accumulator)
*/
(T extends PropertyKey
? Cache
/**
* If T is an object, iterate through each key
*/
: { [P in keyof T]:
(P extends string
/**
* If Cache is an empty string, it means that this call is first,
* because default value of Cache is empty string
*/
? (Cache extends ''
/**
* Since Cache is still empty, no need to use it now
* Just call KeysUnion recursively with first property as an argument for Cache
*/
? KeysUnion<T[P], `${P}`>
/**
* Since Cache is not empty, we need to unionize it with property
* and call KeysUnion recursively again,
* In such way every iteration we apply to Cache new property with dot
*/
: Cache | KeysUnion<T[P], `${Cache}.${P}`>)
: never)
}[keyof T])
// type Result = "one" | "one.two" | "one.two.three" | "one.two.foo"
type Result = KeysUnion<Dict>
您可以找到相关答案:[ , here, , here] and in my article
现在您知道您的命名空间是安全的。我们需要确保我们的第二个(curried)函数需要有效的前缀。我的意思是,如果您提供了 one.two
命名空间,您期望后缀为 three
或 foo
.
为此,我们需要以某种方式从 "one" | "one.two" | "one.two.three" | "one.two.foo"
中提取所有包含 one.two
的键。此外,我们应该摆脱获得的密钥 one.two
前缀,只保留尾部。
我们可以使用这个助手来提取后缀:
type ExtractString<
T extends string,
U extends string,
Result extends string = ''
> =
// infer first char
T extends `${infer Head}${infer Tail}`
/**
* check if concatenated Result and infered char (Head)
* equal to second argument
*/
? `${Result}${Head}` extends U
/**
* if yes - return Tail and close recursion
*/
? Tail
/**
* otherwise, call recursion with Tail (without first char)
* and updated Result
*/
: ExtractString<Tail, U, `${Result}${Head}`>
: Result
/**
* 1) o === one.two ? ExtractString<ne.two.three, 'one.two','o'>
* 2) on === one.two ? ExtractString<e.two.three, 'one.two','on'>
* 2) one === one.two ? ExtractString<.two.three, 'one.two','one'>
* .....
*/
type Test = ExtractString<'one.two.three', 'one.two'>
好的,我们有有效的后缀,但我们需要去掉前导点 .three
:
type RemoveDot<T extends string> = T extends `.${infer Tail}` ? Tail : T;
// three
type Test = RemoveDot<'.three'>
请不要忘记我们需要决定我们需要使用哪些键 ExtractString
。因为我们只需要从提供的命名空间而不是所有键中提取前缀。
type ValidPrefix<
T extends string,
U extends string
> = T extends `${U}${string}` ? Exclude<T, U> : never
// "one.two.three" | "one.two.foo"
type Test = ValidPrefix<'one.two.three' | 'one' | 'one.two.foo', 'one.two'>
我们差不多有自己的类型了。还剩下一件事。我们需要推断我们的 return 类型。
type Values<T> = T[keyof T]
type Elem = string;
type Acc = Record<string, any>
type Predicate<Accumulator extends Acc, El extends Elem> =
El extends keyof Accumulator ? Accumulator[El] : Accumulator
type Reducer<
Keys extends Elem,
Accumulator extends Acc = {}
> =
Keys extends `${infer Prop}.${infer Rest}`
? Reducer<Rest, Predicate<Accumulator, Prop>>
: Keys extends `${infer Last}`
? Predicate<Accumulator, Last>
: never
// 42
type Test = Reducer<'one.two', {
one: {
two: 42
}
}>
你用纯js的例子解释了reducer。
您也可以在我的 article
中找到解释
既然我们已经有了所有需要的工具,我们可以输入我们的函数:
const dict = {
one: {
two: {
three: "3",
foo: "bar"
}
},
bar: {
baz: 2
}
} as const
type Dict = typeof dict
type KeysUnion<T, Cache extends string = ''> =
T extends PropertyKey ? Cache : {
[P in keyof T]:
P extends string
? Cache extends ''
? KeysUnion<T[P], `${P}`>
: Cache | KeysUnion<T[P], `${Cache}.${P}`>
: never
}[keyof T]
type RemoveDot<T extends string> = T extends `.${infer Tail}` ? Tail : T;
type ExtractString<T extends string, U extends string, Result extends string = ''> =
T extends `${infer Head}${infer Tail}` ? `${Result}${Head}` extends U ? Tail : ExtractString<Tail, U, `${Result}${Head}`> : Result
type ValidPrefix<T extends string, U extends string> = T extends `${U}${string}` ? Exclude<T, U> : never
type ConcatNamespaceWithPrefix<N extends string, P extends string> = `${N}.${P}`
type Values<T> = T[keyof T]
type Elem = string;
type Acc = Record<string, any>
// (acc, elem) => hasProperty(acc, elem) ? acc[elem] : acc
type Predicate<Accumulator extends Acc, El extends Elem> =
El extends keyof Accumulator ? Accumulator[El] : Accumulator
type Reducer<
Keys extends Elem,
Accumulator extends Acc = {}
> =
Keys extends `${infer Prop}.${infer Rest}`
? Reducer<Rest, Predicate<Accumulator, Prop>>
: Keys extends `${infer Last}`
? Predicate<Accumulator, Last>
: never
type UseTranslationsProps<D = typeof dict> =
(() => <Prefix extends KeysUnion<D>>(prefix: Prefix) => Reducer<Prefix, D>)
& (
<
ValidKeys extends KeysUnion<D>,
Namespace extends ValidKeys
>(namespace?: Namespace) =>
<Prefix extends RemoveDot<ExtractString<ValidPrefix<KeysUnion<Dict>, Namespace>, Namespace>>>(
prefix: Prefix
) => Reducer<ConcatNamespaceWithPrefix<Namespace, Prefix>, D>
)
declare const useTranslations: UseTranslationsProps;
{
const t = useTranslations() // ok
const ok = t('one') // {two: ....}
}
{
const t = useTranslations('one.two') // ok
const ok = t('three') // 3
}
{
const t = useTranslations() // ok
const ok = t('three') // expected error
}
您可能已经注意到,我在 UseTranslationsProps
中使用了两个函数的交集,它会产生一个重载来调用不带参数的函数。
我正在尝试为 next-intl 包中的函数 useTranslations
添加类型支持。
让我们假设我的 ./locales/en.ts
文件看起来像这样
const dict = {
one: {
two: {
three: "3",
foo: "bar"
}
}
}
export default dict
useTranslations
函数接受命名空间的可选参数。它 returns 另一个函数,调用时具有必需的命名空间参数。通过设置可选参数,它减少了将命名空间的那部分应用到所需参数的需要。这是一个例子:
const t1 = useTranslations() // no optional arg
const val1 = t("one.two.three") // 3
const val2 = t("one.two.foo") // bar
const t2 = useTranslations("one.two")
const val3 = t("three") // 3
const val4 = t("foo") // bar
我已经设法让类型工作以创建对象的字符串表示形式,但我在第一个参数继续到第二个时遇到问题。
到目前为止,这是我的代码
import dict from "./locales/en"
// `T` is the dictionary, `S` is the next string part of the object property path
// If `S` does not match dict shape, return its next expected properties
type DeepKeys<T, S extends string> = T extends object
? S extends `${infer I1}.${infer I2}`
? I1 extends keyof T
? `${I1}.${DeepKeys<T[I1], I2>}`
: keyof T & string
: S extends keyof T
? `${S}`
: keyof T & string
: ""
interface UseTranslationsReturn<D> {
<S extends string>(
namespace: DeepKeys<D, S>,
values?: Record<
string,
| string
| number
| boolean
| Date
| ((children: ReactNode) => ReactNode)
| null
| undefined
>
): string
}
interface UseTranslationsProps<D = typeof dict> {
<S extends string>(namespace?: DeepKeys<D, S>): UseTranslationsReturn<D>
}
export const useTranslations: UseTranslationsProps = (namespace) => useNextTranslations(namespace)
我可以像这样设置主要功能,
const t = useTranslations("one.two")
但是我得到一个错误,
const val = t("three") // Argument of type '"three"' is not assignable to parameter of type "one"
如何调整类型来解决这个问题?
首先,useTranslations
应该期望有效的路径点符号。我的意思是它应该期望 "one" | "one.two" | "one.two.three" | "one.two.foo"
的并集。你会在评论中找到解释:
const dict = {
one: {
two: {
three: "3",
foo: "bar"
}
}
} as const
type Dict = typeof dict
type KeysUnion<T, Cache extends string = ''> =
/**
* If T is no more an object, it means that this call
* is last in a recursion, hence we need to return
* our Cache (accumulator)
*/
(T extends PropertyKey
? Cache
/**
* If T is an object, iterate through each key
*/
: { [P in keyof T]:
(P extends string
/**
* If Cache is an empty string, it means that this call is first,
* because default value of Cache is empty string
*/
? (Cache extends ''
/**
* Since Cache is still empty, no need to use it now
* Just call KeysUnion recursively with first property as an argument for Cache
*/
? KeysUnion<T[P], `${P}`>
/**
* Since Cache is not empty, we need to unionize it with property
* and call KeysUnion recursively again,
* In such way every iteration we apply to Cache new property with dot
*/
: Cache | KeysUnion<T[P], `${Cache}.${P}`>)
: never)
}[keyof T])
// type Result = "one" | "one.two" | "one.two.three" | "one.two.foo"
type Result = KeysUnion<Dict>
您可以找到相关答案:[
现在您知道您的命名空间是安全的。我们需要确保我们的第二个(curried)函数需要有效的前缀。我的意思是,如果您提供了 one.two
命名空间,您期望后缀为 three
或 foo
.
为此,我们需要以某种方式从 "one" | "one.two" | "one.two.three" | "one.two.foo"
中提取所有包含 one.two
的键。此外,我们应该摆脱获得的密钥 one.two
前缀,只保留尾部。
我们可以使用这个助手来提取后缀:
type ExtractString<
T extends string,
U extends string,
Result extends string = ''
> =
// infer first char
T extends `${infer Head}${infer Tail}`
/**
* check if concatenated Result and infered char (Head)
* equal to second argument
*/
? `${Result}${Head}` extends U
/**
* if yes - return Tail and close recursion
*/
? Tail
/**
* otherwise, call recursion with Tail (without first char)
* and updated Result
*/
: ExtractString<Tail, U, `${Result}${Head}`>
: Result
/**
* 1) o === one.two ? ExtractString<ne.two.three, 'one.two','o'>
* 2) on === one.two ? ExtractString<e.two.three, 'one.two','on'>
* 2) one === one.two ? ExtractString<.two.three, 'one.two','one'>
* .....
*/
type Test = ExtractString<'one.two.three', 'one.two'>
好的,我们有有效的后缀,但我们需要去掉前导点 .three
:
type RemoveDot<T extends string> = T extends `.${infer Tail}` ? Tail : T;
// three
type Test = RemoveDot<'.three'>
请不要忘记我们需要决定我们需要使用哪些键 ExtractString
。因为我们只需要从提供的命名空间而不是所有键中提取前缀。
type ValidPrefix<
T extends string,
U extends string
> = T extends `${U}${string}` ? Exclude<T, U> : never
// "one.two.three" | "one.two.foo"
type Test = ValidPrefix<'one.two.three' | 'one' | 'one.two.foo', 'one.two'>
我们差不多有自己的类型了。还剩下一件事。我们需要推断我们的 return 类型。
type Values<T> = T[keyof T]
type Elem = string;
type Acc = Record<string, any>
type Predicate<Accumulator extends Acc, El extends Elem> =
El extends keyof Accumulator ? Accumulator[El] : Accumulator
type Reducer<
Keys extends Elem,
Accumulator extends Acc = {}
> =
Keys extends `${infer Prop}.${infer Rest}`
? Reducer<Rest, Predicate<Accumulator, Prop>>
: Keys extends `${infer Last}`
? Predicate<Accumulator, Last>
: never
// 42
type Test = Reducer<'one.two', {
one: {
two: 42
}
}>
既然我们已经有了所有需要的工具,我们可以输入我们的函数:
const dict = {
one: {
two: {
three: "3",
foo: "bar"
}
},
bar: {
baz: 2
}
} as const
type Dict = typeof dict
type KeysUnion<T, Cache extends string = ''> =
T extends PropertyKey ? Cache : {
[P in keyof T]:
P extends string
? Cache extends ''
? KeysUnion<T[P], `${P}`>
: Cache | KeysUnion<T[P], `${Cache}.${P}`>
: never
}[keyof T]
type RemoveDot<T extends string> = T extends `.${infer Tail}` ? Tail : T;
type ExtractString<T extends string, U extends string, Result extends string = ''> =
T extends `${infer Head}${infer Tail}` ? `${Result}${Head}` extends U ? Tail : ExtractString<Tail, U, `${Result}${Head}`> : Result
type ValidPrefix<T extends string, U extends string> = T extends `${U}${string}` ? Exclude<T, U> : never
type ConcatNamespaceWithPrefix<N extends string, P extends string> = `${N}.${P}`
type Values<T> = T[keyof T]
type Elem = string;
type Acc = Record<string, any>
// (acc, elem) => hasProperty(acc, elem) ? acc[elem] : acc
type Predicate<Accumulator extends Acc, El extends Elem> =
El extends keyof Accumulator ? Accumulator[El] : Accumulator
type Reducer<
Keys extends Elem,
Accumulator extends Acc = {}
> =
Keys extends `${infer Prop}.${infer Rest}`
? Reducer<Rest, Predicate<Accumulator, Prop>>
: Keys extends `${infer Last}`
? Predicate<Accumulator, Last>
: never
type UseTranslationsProps<D = typeof dict> =
(() => <Prefix extends KeysUnion<D>>(prefix: Prefix) => Reducer<Prefix, D>)
& (
<
ValidKeys extends KeysUnion<D>,
Namespace extends ValidKeys
>(namespace?: Namespace) =>
<Prefix extends RemoveDot<ExtractString<ValidPrefix<KeysUnion<Dict>, Namespace>, Namespace>>>(
prefix: Prefix
) => Reducer<ConcatNamespaceWithPrefix<Namespace, Prefix>, D>
)
declare const useTranslations: UseTranslationsProps;
{
const t = useTranslations() // ok
const ok = t('one') // {two: ....}
}
{
const t = useTranslations('one.two') // ok
const ok = t('three') // 3
}
{
const t = useTranslations() // ok
const ok = t('three') // expected error
}
您可能已经注意到,我在 UseTranslationsProps
中使用了两个函数的交集,它会产生一个重载来调用不带参数的函数。