Автор: Цуй Цзин
Сама по себе функция defineComponent очень проста, но основная функция — протолкнуть тип под ts. Для ts-файла, если мы напишем напрямую
export default {}
В настоящее время для редактора {} — это просто тип объекта, и мы не можем специально подсказать нам, какие свойства должны быть в {} для компонента vue. Но если вы добавите слой defineComponent,
export default defineComponent({})
В это время {} становится параметром defineComponent, тогда подсказка типа параметра может реализовать подсказку атрибута в {}, а также может выполнять вывод некоторого типа и другие операции над параметром.
Но в приведенном выше примере, если вы попробуете его в файле .vue vscode, вы обнаружите, что есть также подсказка без написания defineComponent. На самом деле этим занимается плагин Vetur.
Давайте посмотрим на реализацию defineComponent.Есть 4 перегрузки.Посмотрим сначала на самую простую.Мне все равно,что здесь DefineComponent,и мы ее рассмотрим позже.
// overload 1: direct setup function
// (uses user defined props interface)
export function defineComponent<Props, RawBindings = object>(
setup: (
props: Readonly<Props>,
ctx: SetupContext
) => RawBindings | RenderFunction
): DefineComponent<Props, RawBindings>
Параметр defineComponet — функция, функция имеет два параметра props и ctx, а тип возвращаемого значения — RawBindings или RenderFunction. Тип возвращаемого значения для defineComponent:DefineComponent<Props, RawBindings>
. Среди них два общих реквизита и RawBindings. Пропсы будут определяться в соответствии с типом, переданным в первый параметр настройки, когда мы на самом деле пишем, а RawBindings будут определяться в соответствии с возвращаемым значением нашей настройки. Длинный абзац более запутан, напишите аналогичный простой пример, чтобы увидеть:
-
Простая демонстрация, похожая на использование props, выглядит следующим образом: мы передаем различные типы параметров в a, и типы возвращаемых значений define также различны. это называетсяGeneric Functions
declare function define<Props>(a: Props): Props const arg1:string = '123' const result1 = define(arg1) // result1:string const arg2:number = 1 const result2 = define(arg2) // result2: number
-
Простая демонстрация, похожая на RawBindings, выглядит следующим образом: тип возвращаемого значения установки отличается, и тип возвращаемого значения определения также отличается.
declare function define<T>(setup: ()=>T): T const arg1:string = '123' const resul1 = define(() => { return arg1 }) const arg2:number = 1 const result2 = define(() => { return arg2 })
Из приведенных выше двух простых демонстраций мы можем понять значение перегрузки 1. Тип возвращаемого значения для defineComponent:DefineComponent<Props, RawBindings>
, где Props – это первый тип параметра настройки, RawBindings – тип возвращаемого значения настройки. Если мы возвращаем значение в функцию, значением по умолчанию является объект. Из него вы можете понять основное использование вывода ts для следующего определения
declare function define<T>(a: T): T
Тип T может быть динамически определен в соответствии с параметрами, переданными во время выполнения.Этот метод также является единственной связью между типами времени выполнения и статическими типами typescript.Много раз мы хотим передать типы параметров во время выполнения, чтобы определить другие связанные типы, мы можем использовать этот метод.
Затем посмотрите на definComponent, его перегрузки 2, 3 и 4 предназначены для работы с различными типами свойств в параметрах. Посмотрите объявления самых распространенных реквизитов типа object
export function defineComponent<
// the Readonly constraint allows TS to treat the type of { required: true }
// as constant instead of boolean.
PropsOptions extends Readonly<ComponentPropsOptions>,
RawBindings,
D,
C extends ComputedOptions = {},
M extends MethodOptions = {},
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = Record<string, any>,
EE extends string = string
>(
options: ComponentOptionsWithObjectProps<
PropsOptions,
RawBindings,
D,
C,
M,
Mixin,
Extends,
E,
EE
>
): DefineComponent<PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE>
Подобно идее перегрузки 1 выше, основная идея состоит в том, чтобы получить различные дженерики на основе содержимого опций, записанных во время выполнения. Первый параметр настройки в vue3 — реквизит, и тип этого реквизита должен совпадать с тем, что мы передали в параметрах. это вComponentOptionsWithObjectProps
中实现的。 код показывает, как показано ниже
export type ComponentOptionsWithObjectProps<
PropsOptions = ComponentObjectPropsOptions,
RawBindings = {},
D = {},
C extends ComputedOptions = {},
M extends MethodOptions = {},
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = EmitsOptions,
EE extends string = string,
Props = Readonly<ExtractPropTypes<PropsOptions>>,
Defaults = ExtractDefaultPropTypes<PropsOptions>
> = ComponentOptionsBase<
Props,
RawBindings,
D,
C,
M,
Mixin,
Extends,
E,
EE,
Defaults
> & {
props: PropsOptions & ThisType<void>
} & ThisType<
CreateComponentPublicInstance<
Props,
RawBindings,
D,
C,
M,
Mixin,
Extends,
E,
Props,
Defaults,
false
>
>
export interface ComponentOptionsBase<
Props,
RawBindings,
D,
C extends ComputedOptions,
M extends MethodOptions,
Mixin extends ComponentOptionsMixin,
Extends extends ComponentOptionsMixin,
E extends EmitsOptions,
EE extends string = string,
Defaults = {}
>
extends LegacyOptions<Props, D, C, M, Mixin, Extends>,
ComponentInternalOptions,
ComponentCustomOptions {
setup?: (
this: void,
props: Props,
ctx: SetupContext<E, Props>
) => Promise<RawBindings> | RawBindings | RenderFunction | void
//...
}
Это длинный абзац, давайте воспользуемся упрощенной версией демо, чтобы понять то же самое:
type TypeA<T1, T2, T3> = {
a: T1,
b: T2,
c: T3
}
declare function define<T1, T2, T3>(options: TypeA<T1, T2, T3>): T1
const result = define({
a: '1',
b: 1,
c: {}
}) // result: string
В соответствии с параметром входящих опций ts будут определены типы T1, T2 и T3. Получив T1, T2, T3, вы можете использовать их для других выводов. Немного изменим демонстрацию выше, предполагая, что c — это функция, а тип параметра в ней определяется типом a:
type TypeA<T1, T2, T3> = TypeB<T1, T2>
type TypeB<T1, T2> = {
a: T1
b: T2,
c: (arg:T1)=>{}
}
const result = define({
a: '1',
b: 1,
c: (arg) => { // arg 这里就被会推导为一个 string 的类型
return arg
}
})
Затем посмотрите код в vue, сначалаdefineComponent
PropsOptions могут быть получены. Но если props — это объектный тип, то он записывается следующим образом
props: {
name: {
type: String,
//... 其他的属性
}
}
Для параметра props в настройке нужно извлечь из него тип. Итак, в ComponentOptionsWithObjectProps
export type ComponentOptionsWithObjectProps<
PropsOptions = ComponentObjectPropsOptions,
//...
Props = Readonly<ExtractPropTypes<PropsOptions>>,
//...
>
Тип пропсоотписей по преобразованию ExtreacPropTypes, а затем, чтобы дать реквизиты, затем пройти компонентОтриологии, внутри, в качестве параметра настройки
export interface ComponentOptionsBase<
Props,
//...
>
extends LegacyOptions<Props, D, C, M, Mixin, Extends>,
ComponentInternalOptions,
ComponentCustomOptions {
setup?: (
this: void,
props: Props,
ctx: SetupContext<E, Props>
) => Promise<RawBindings> | RawBindings | RenderFunction | void
Это реализация вывода реквизита.
-
что это делает
Первым в определении установки является: void . Когда мы напишем логику в функции установки, мы обнаружим, что если использовать
this.xxx
В IDE будут ошибкиProperty 'xxx' does not exist on type 'void'
Здесь, установив
this:void
чтобы мы не использовали это в настройке.это особое существование в js, оно определяется в соответствии с текущим контекстом, поэтому иногда машинописный текст не может точно определить, какой тип this используется в нашем коде, поэтому это становится любым, каждым Все виды подсказок/выводов типов не могут быть used (примечание: ts выведет тип this только в том случае, если включена конфигурация noImplicitThis). Чтобы решить эту проблему, функция в машинописном тексте может явно написать параметр this, как в примере на официальном сайте:
interface Card { suit: string; card: number; } interface Deck { suits: string[]; cards: number[]; createCardPicker(this: Deck): () => Card; } let deck: Deck = { suits: ["hearts", "spades", "clubs", "diamonds"], cards: Array(52), // NOTE: The function now explicitly specifies that its callee must be of type Deck createCardPicker: function (this: Deck) { return () => { let pickedCard = Math.floor(Math.random() * 52); let pickedSuit = Math.floor(pickedCard / 13); return { suit: this.suits[pickedSuit], card: pickedCard % 13 }; }; }, }; let cardPicker = deck.createCardPicker(); let pickedCard = cardPicker(); alert("card: " + pickedCard.card + " of " + pickedCard.suit);
В createCardPicker явно указано, что это тип Deck. так в
createCardPicker
Свойства/методы, которые можно использовать при этом, ограничены теми, что есть в колоде.Кроме того, в связи с этим есть еще одно
ThisType
.
ExtractPropTypes и ExtractDefaultPropTypes
Как упоминалось выше, реквизит, который мы написали
{
props: {
name1: {
type: String,
require: true
},
name2: {
type: Number
}
}
}
После определенного диривации он преобразуется в тип TS
ReadOnly<{
name1: string,
name2?: number | undefined
}>
Этот процесс реализован с помощью ExtractPropTypes.
export type ExtractPropTypes<O> = O extends object
? { [K in RequiredKeys<O>]: InferPropType<O[K]> } &
{ [K in OptionalKeys<O>]?: InferPropType<O[K]> }
: { [K in string]: any }
Хорошо понятен на основе четкого именования в типе: useRrequiredKeys<O>
а такжеOptionsKeys<O>
Разделите O в зависимости от того, требуется ли это (в качестве примера возьмем предыдущие реквизиты)
{
name1
} & {
name2?
}
Затем в каждой группе используйтеInferPropType<O[K]>
Выведите тип.
-
InferPropType
Прежде чем понять это, понять некоторые простые производные. Сначала мы пишем в коде
props = { type: String }
Если TS получен, тип пророков - Струйтконструктор. Таким образом, первый шаг требуется для получения соответствующей строки типа / номер et al. Может быть реализован с помощью
type a = StringConstructor type ConstructorToType<T> = T extends { (): infer V } ? V : never type c = ConstructorToType<a> // type c = String
выше мы проходим
():infer V
чтобы получить тип. Причина, по которой его можно использовать таким образом, связана с реализацией таких типов, как String/Number. можно написать на javascriptconst key = String('a')
На данный момент ключ имеет строковый тип. Также взгляните на представление типа интерфейса StringConstructor.
interface StringConstructor { new(value?: any): String; (value?: any): string; readonly prototype: String; fromCharCode(...codes: number[]): string; }
Eсть
():string
, так чтоextends {(): infer V}
Представленная V - это строка.Затем далее измените вышеупомянутое изменение к контенту в пропсоптиках, а затем обратитесь к предположению V в конструкторине для определения
type a = StringConstructor type ConstructorType<T> = { (): T } type b = a extends { type: ConstructorType<infer V> required?: boolean } ? V : never // type b = String
Таким образом, содержимое реквизита легко преобразовать в тип в типе.
Поскольку этот тип реквизита поддерживает многие методы письма на китайском языке, фактическая реализация кода в vue3 более сложна.
type InferPropType<T> = T extends null ? any // null & true would fail to infer : T extends { type: null | true } ? any // As TS issue https://github.com/Microsoft/TypeScript/issues/14829 // somehow `ObjectConstructor` when inferred from { (): T } becomes `any` // `BooleanConstructor` when inferred from PropConstructor(with PropMethod) becomes `Boolean` // 这里单独判断了 ObjectConstructor 和 BooleanConstructor : T extends ObjectConstructor | { type: ObjectConstructor } ? Record<string, any> : T extends BooleanConstructor | { type: BooleanConstructor } ? boolean : T extends Prop<infer V, infer D> ? (unknown extends V ? D : V) : T // 支持 PropOptions 和 PropType 两种形式 type Prop<T, D = T> = PropOptions<T, D> | PropType<T> interface PropOptions<T = any, D = T> { type?: PropType<T> | true | null required?: boolean default?: D | DefaultFactory<D> | null | undefined | object validator?(value: unknown): boolean } export type PropType<T> = PropConstructor<T> | PropConstructor<T>[] type PropConstructor<T = any> = | { new (...args: any[]): T & object } // 可以匹配到其他的 Constructor | { (): T } // 可以匹配到 StringConstructor/NumberConstructor 和 () => string 等 | PropMethod<T> // 匹配到 type: (a: number, b: string) => string 等 Function 的形式 // 对于 Function 的形式,通过 PropMethod 构造成了一个和 stringConstructor 类型的类型 // PropMethod 作为 PropType 类型之一 // 我们写 type: Function as PropType<(a: string) => {b: string}> 的时候,就会被转化为 // type: (new (...args: any[]) => ((a: number, b: string) => { // a: boolean; // }) & object) | (() => (a: number, b: string) => { // a: boolean; // }) | { // (): (a: number, b: string) => { // a: boolean; // }; // new (): any; // readonly prototype: any; // } // 然后在 InferPropType 中就可以推断出 (a:number,b:string)=> {a: boolean} type PropMethod<T, TConstructor = any> = T extends (...args: any) => any // if is function with args ? { new (): TConstructor; (): T; readonly prototype: TConstructor } // Create Function like constructor : never
-
RequiredKeys
Это используется для отделения ключа, который должен иметь значение от реквизита.Исходный код выглядит следующим образом
type RequiredKeys<T> = { [K in keyof T]: T[K] extends | { required: true } | { default: any } // don't mark Boolean props as undefined | BooleanConstructor | { type: BooleanConstructor } ? K : never }[keyof T]
Помимо явного определения reqruied, он также содержит значение по умолчанию или логический тип. Потому что для логического значения, если мы его не передаем, оно по умолчанию равно false, а реквизиты со значениями по умолчанию не должны быть неопределенными.
-
OptionalKeys
С RequiredKeys, OptionsKeys легко: просто исключите RequiredKeys
type OptionalKeys<T> = Exclude<keyof T, RequiredKeys<T>>
ExtractDefaultPropTypes похож на ExtractPropTypes, поэтому я не буду его писать.
Получение возвращаемого значения метода, вычисляемого и данных в параметрах аналогично получению реквизитов выше.
emits options
К параметрам vue3 добавлена новая конфигурация эммитов, которая может отображать конфигурацию событий, которые мы хотим отправить в компоненте. События настроены в эммитах, когда мы пишем$emit
, он будет предложен как первый параметр функции.
Способ получения значений конфигурации в emits аналогичен получению типов в props выше.$emit
, это черезThisType
Для достижения (обратитесь к другой статье об ThisType). Ниже приведена упрощенная демонстрация
declare function define<T>(props:{
emits: T,
method?: {[key: string]: (...arg: any) => any}
} & ThisType<{
$emits: (arg: T) => void
}>):T
const result = define({
emits: {
key: '123'
},
method: {
fn() {
this.$emits(/*这里会提示:arg: {
key: string;
}*/)
}
}
})
Вышеприведенное выводит T как тип в emits. потом& ThisType
, чтобы его можно было использовать в методеthis.$emit
. Затем используйте T в качестве типа параметра $emit, вы можете написатьthis.$emit
когда будет предложено.
Затем посмотрите на реализацию в vue3
export function defineComponent<
//... 省却其他的
E extends EmitsOptions = Record<string, any>,
//...
>(
options: ComponentOptionsWithObjectProps<
//...
E,
//...
>
): DefineComponent<PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE>
export type ComponentOptionsWithObjectProps<
//..
E extends EmitsOptions = EmitsOptions,
//...
> = ComponentOptionsBase< // 定义一个 E 的泛型
//...
E,
//...
> & {
props: PropsOptions & ThisType<void>
} & ThisType<
CreateComponentPublicInstance< // 利用 ThisType 实现 $emit 中的提示
//...
E,
//...
>
>
// ComponentOptionsWithObjectProps 中 包含了 ComponentOptionsBase
export interface ComponentOptionsBase<
//...
E extends EmitsOptions, // type EmitsOptions = Record<string, ((...args: any[]) => any) | null> | string[]
EE extends string = string,
Defaults = {}
>
extends LegacyOptions<Props, D, C, M, Mixin, Extends>,
ComponentInternalOptions,
ComponentCustomOptions {
//..
emits?: (E | EE[]) & ThisType<void> // 推断出 E 的类型
}
export type ComponentPublicInstance<
//...
E extends EmitsOptions = {},
//...
> = {
//...
$emit: EmitFn<E> // EmitFn 来提取出 E 中的 key
//...
}
Шагая в яму, учась и практикуясь. Переходим к пит-процессу: реализуем процесс деривации эмиссий
export type ObjectEmitsOptions = Record<
string,
((...args: any[]) => any) | null
>
export type EmitsOptions = ObjectEmitsOptions | string[];
declare function define<E extends EmitsOptions = Record<string, any>, EE extends string = string>(options: E| EE[]): (E | EE[]) & ThisType<void>
Затем используйте следующий способ, чтобы проверить результат
const emit = ['key1', 'key2']
const a = define(emit)
Глядя на подсказку ts, я обнаружил, что тип aconst b: string[] & ThisType<void>
, но на самом деле, если тот же массив записан в vue3, подсказкаconst a: (("key1" | "key2")[] & ThisType<void>) | (("key1" | "key2")[] & ThisType<void>)
После долгой борьбы я наконец нашел разницу в способе написания: результаты согласуются со следующими словами.
define(['key1', 'key2'])
Но используя предыдущий метод записи, при передаче через переменную, когда ts получает emit, его тип выводится какstring[]
, поэтому тип, полученный в функции определения, становитсяstring[]
, вместо оригинала['key1', 'key2']
Поэтому необходимо обратить внимание: при определении эммитов в vue3 рекомендуется писать прямо в эммитах, а не извлекать его как отдельную переменную и потом передавать в эммиты
Если его действительно нужно поместить в отдельную переменную, его нужно обработать так, чтобы'[key1', 'key2']
Тип возврата определения переменной:['key1', 'key2']
вместоstring[]
. Вы можете использовать следующие два способа:
-
метод первый
const keys = ["key1", "key2"] as const; // const keys: readonly ["key1", "key2"]
Так проще написать. Но есть недостаток, ключи конвертируются в readonly, и ключи нельзя модифицировать потом.
Справочная статья2 ways to create a Union from an Array in Typescript
-
Способ 2
type UnionToIntersection<T> = (T extends any ? (v: T) => void : never) extends (v: infer V) => void ? V : never type LastOf<T> = UnionToIntersection<T extends any ? () => T : never> extends () => infer R ? R : never type Push<T extends any[], V> = [ ...T, V] type UnionToTuple<T, L = LastOf<T>, N = [T] extends [never] ? true : false> = N extends true ? [] : Push<UnionToTuple<Exclude<T, L>>, L> declare function tuple<T extends string>(arr: T[]): UnionToTuple<T> const c = tuple(['key1', 'key2']) // const c: ["key1", "key2"]
сначала через
arr: T[]
Буду['key1', 'key2']
Преобразовать в объединение, а затем рекурсивно,LastOf
получить последний в союзе,Push
в массив.
миксины и расширения
Контент, написанный в миксинах или расширениях в vue3, можно найти вthis
подскажите в. Для примесей и расширений есть одно большое отличие от других типов вывода выше: рекурсия. Следовательно, рекурсивная обработка также требуется при вынесении суждений о типах. Простой пример выглядит следующим образом
const AComp = {
methods: {
someA(){}
}
}
const BComp = {
mixins: [AComp],
methods: {
someB() {}
}
}
const CComp = {
mixins: [BComp],
methods: {
someC() {}
}
}
Для намека на это в CComp должны быть методы someB и someA. Чтобы реализовать эту подсказку, при выполнении вывода типа требуется ThisType, подобный следующему.
ThisType<{
someA
} & {
someB
} & {
someC
}>
Итак, для обработки миксинов необходимо рекурсивно получить содержимое миксинов в компоненте, а затем преобразовать вложенные типы в плоские, которые связаны &. См. реализацию в исходном коде:
// 判断 T 中是否有 mixin
// 如果 T 含有 mixin 那么这里结果为 false,以为 {mixin: any} {mixin?: any} 是无法互相 extends 的
type IsDefaultMixinComponent<T> = T extends ComponentOptionsMixin
? ComponentOptionsMixin extends T ? true : false
: false
//
type IntersectionMixin<T> = IsDefaultMixinComponent<T> extends true
? OptionTypesType<{}, {}, {}, {}, {}> // T 不包含 mixin,那么递归结束,返回 {}
: UnionToIntersection<ExtractMixin<T>> // 获取 T 中 Mixin 的内容进行递归
// ExtractMixin(map type) is used to resolve circularly references
type ExtractMixin<T> = {
Mixin: MixinToOptionTypes<T>
}[T extends ComponentOptionsMixin ? 'Mixin' : never]
// 通过 infer 获取到 T 中 Mixin, 然后递归调用 IntersectionMixin<Mixin>
type MixinToOptionTypes<T> = T extends ComponentOptionsBase<
infer P,
infer B,
infer D,
infer C,
infer M,
infer Mixin,
infer Extends,
any,
any,
infer Defaults
>
? OptionTypesType<P & {}, B & {}, D & {}, C & {}, M & {}, Defaults & {}> &
IntersectionMixin<Mixin> &
IntersectionMixin<Extends>
: never
Процесс одинаков для расширений и миксинов. Затем посмотрите на обработку в ThisType
ThisType<
CreateComponentPublicInstance<
Props,
RawBindings,
D,
C,
M,
Mixin,
Extends,
E,
Props,
Defaults,
false
>
>
export type CreateComponentPublicInstance<
P = {},
B = {},
D = {},
C extends ComputedOptions = {},
M extends MethodOptions = {},
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = {},
PublicProps = P,
Defaults = {},
MakeDefaultsOptional extends boolean = false,
// 将嵌套的结构转为扁平化的
PublicMixin = IntersectionMixin<Mixin> & IntersectionMixin<Extends>,
// 提取 props
PublicP = UnwrapMixinsType<PublicMixin, 'P'> & EnsureNonVoid<P>,
// 提取 RawBindings,也就是 setup 返回的内容
PublicB = UnwrapMixinsType<PublicMixin, 'B'> & EnsureNonVoid<B>,
// 提取 data 返回的内容
PublicD = UnwrapMixinsType<PublicMixin, 'D'> & EnsureNonVoid<D>,
PublicC extends ComputedOptions = UnwrapMixinsType<PublicMixin, 'C'> &
EnsureNonVoid<C>,
PublicM extends MethodOptions = UnwrapMixinsType<PublicMixin, 'M'> &
EnsureNonVoid<M>,
PublicDefaults = UnwrapMixinsType<PublicMixin, 'Defaults'> &
EnsureNonVoid<Defaults>
> = ComponentPublicInstance< // 上面结果传给 ComponentPublicInstance,生成 this context 中的内容
PublicP,
PublicB,
PublicD,
PublicC,
PublicM,
E,
PublicProps,
PublicDefaults,
MakeDefaultsOptional,
ComponentOptionsBase<P, B, D, C, M, Mixin, Extends, E, string, Defaults>
>
Выше приведена реализация большей части defineComponent в целом.Видно, что он был рожден исключительно для вывода типов.В то же время здесь используется много типов методов вывода, а некоторые здесь не рассматриваются.Заинтересованные студенты можно перейти к более подробному рассмотрению реализации в Vue.