javascript - TS: สืบทอดชนิดค่าคงที่ตามวิธีที่ต่อกัน
เหลือเพียงสิ่งเดียวสำหรับฉันในการปลดล็อกการไหลและเผยแพร่เวอร์ชันทดลองแรกของไลบรารีการตรวจสอบฟอร์มของฉัน
ฉันมีโค้ดดังต่อไปนี้ (แน่นอนว่าฉันละเว้นหลายสิ่งหลายอย่างเพื่อไม่ให้มันใหญ่เกินไป)
interface Validation {
name: string
message: string
params?: Record<string, any>
test: (value: any, params?: any) => boolean
}
class MixedSchema<T> {
type!: T
validations: Validation[] = []
required(message?: string) {
this.validations.push({
name: 'required',
message: message,
test: (value: any) => {
return value === '' ? false : true
},
})
return this
}
oneOf(arrayOfValues: any[], message?: string) {
type Params = { arrayOfValues: any[] }
this.validations.push({
name: 'oneOf',
params: { arrayOfValues },
message: message,
test: (value: any, params: Params) => {
return params.arrayOfValues.includes(value)
},
})
return this
}
}
class StringSchema extends MixedSchema<string> {
email() {
// this.validations.push({ ... })
return this
}
maxWords(maxWords: number, message?: string) {
// this.validations.push({ ... })
return this
}
}
class NumberSchema extends MixedSchema<number> {
min(min: number, message?: string) {
// this.validations.push({ ... })
return this
}
round(type: 'toUp' | 'toDown' | 'closer') {
// this.validations.push({ ... })
return this
}
}
const schema = {
string() {
return new StringSchema()
},
number() {
return new NumberSchema()
},
// array, file, etc..
}
const form = {
email: schema.string().required(),
age: schema.number().min(18),
gender: schema.string().oneOf(['male', 'female']).required(),
color: schema.string().oneOf(['red', 'blue', 'green']),
}
type InferType<T> = {
[P in keyof T]: T[P] extends MixedSchema<infer TS> ? TS : never
}
type Form = InferType<typeof form>
ดังนั้นฉันจึงได้ผลลัพธ์ดังต่อไปนี้
แต่ฉันต้องการให้ได้การพิมพ์ที่สมจริงที่สุดเท่าที่จะเป็นไปได้ หมายความว่า คล้ายกับแบบแผนที่กำหนดไว้สำหรับ form ตัวอย่าง
interface HowNeedsToBe {
email: string
age: number | undefined
gender: 'male' | 'female'
color: 'red' | 'blue' | 'green' | undefined
}
ฉันเชื่อว่าเหตุผลคือประมาณว่า ถ้าไม่มี required ก็ใส่ undefined และถ้ามี oneOf ก็แทนที่อาร์กิวเมนต์ด้วย T ของ MixedSchema<T> แต่ฉันไม่รู้ว่าจะส่งสิ่งนี้กลับไปยัง MixedSchema<T> ได้อย่างไร จริงๆ แล้วฉันคิดว่าเหตุผลนี้มันยุ่งเหยิง
ฉันได้ทำการค้นคว้าเกี่ยวกับ map และ generics ใน TypeScript แล้ว แต่ฉันต้องยอมรับว่าเมื่อถึงเวลานำไปใช้จริง ไม่มีอะไรดีออกมาเลย
นี่คือสนามเด็กเล่น TS ถ้าคุณต้องการลอง
คำตอบ #1
ในเชิงแนวคิด คุณต้องการให้เมธอด required() และ oneOf() ลดขอบเขตของ T; ซึ่งการให้ประเภทกับมันคงไม่ยากนัก (แม้ว่าคุณจะต้องใช้การยืนยันประเภทเพื่อหลีกเลี่ยงข้อผิดพลาดของคอมไพเลอร์ เนื่องจากคอมไพเลอร์ไม่สามารถตรวจสอบได้ว่าคุณได้ทำการลดขอบเขตตามที่ต้องการจริง ๆ) ดังนั้น required() เมื่อถูกเรียกบน MixedSchema<T> ควรคืนค่า MixedSchema<Exclude<T, undefined>> (ใช้ยูทิลิตี้ประเภท Exclude เพื่อลบ undefined ออกจากสมาชิกยูเนียนใด ๆ ของ T) และ oneOf() ควรเป็นเจนเนอริกในประเภทขององค์ประกอบ U ของสมาชิกของ arrayOfValues และควรคืนค่า MixedSchema<U | Extract<undefined, T>> (ใช้ยูทิลิตี้ประเภท Extract เพื่อเก็บ undefined หาก T อาจเป็น undefined)
นี่คือลักษณะที่มันอาจปรากฏ (การใช้งานถูกละไว้เพื่อความกระชับ):
declare class MixedSchema<T> {
type: T
validations: Validation[];
required(message?: string): MixedSchema<Exclude<T, undefined>>;
oneOf<U extends T>(arrayOfValues: U[], message?: string):
MixedSchema<U | Extract<undefined, T>>
}
น่าเสียดายที่การที่คุณสืบทอดจาก MixedSchema ทำให้เรื่องซับซ้อนขึ้น; คุณต้องการที่จะบอกว่า ตัวอย่างเช่น StringSchema ควรที่จะคงเป็น StringSchema ประเภทใดประเภทหนึ่งหลังจากเรียก required(); มันไม่ควรถูกขยายกลับไปเป็น MixedSchema:
declare class StringSchema<T extends string | undefined> extends MixedSchema<T> {
email(): this;
maxWords(maxWords: number, message?: string): this;
}
declare class NumberSchema<T extends number | undefined> extends MixedSchema<T> {
min(min: number, message?: string): this;
round(type: 'toUp' | 'toDown' | 'closer'): this;
}
const s = new StringSchema() // StringSchema<string | undefined>
s.email(); // okay
const t = s.required(); // MixedSchema<string>
t.email(); // error! Property 'email' does not exist on type 'MixedSchema<string>';
ดังนั้นเราจะต้องใช้สิ่งที่ซับซ้อนมากขึ้น
คำตอบที่ “ถูกต้อง” ที่นี่คือการใช้สิ่งที่เรียกว่า higher-kinded types ประเภทที่ถูกขอ (แต่ยังไม่ได้ถูกนำไปใช้) ใน microsoft/TypeScript#1213 คุณอาจอยากจะพูดอะไรบางอย่างเช่น: เมธอด required() ของ MixedSchema<T> ควรคืนค่า this<Exclude<T, undefined>> ซึ่งคุณกำลังพยายามใช้ this เหมือนเป็นชนิดที่รับพารามิเตอร์เจนเนอริก ดังนั้นถ้า this เป็น StringSchema<T> มันควรจะเป็น StringSchema<Exclude<T, undefined>> แต่ไม่มีการสนับสนุนตรงๆ สำหรับสิ่งนี้
แทนที่จะเป็นเช่นนั้น เราจำเป็นต้องจำลองมัน และการจำลองทั้งหมดเช่นนี้จะเกี่ยวข้องกับการ "ลงทะเบียน" ประเภทที่เราต้องการที่จะสามารถปฏิบัติเหมือนเป็น generic ของ generic:
type Specify<C extends MixedSchema<any>, T> =
C extends NumberSchema<any> ? NumberSchema<Extract<T, number | undefined>> :
C extends StringSchema<any> ? StringSchema<Extract<T, string | undefined>> :
MixedSchema<T>;
เราได้ระบุรายการย่อยคลาสทั้งหมดของ MixedSchema ที่เราสนใจและอธิบายวิธีการระบุพารามิเตอร์ชนิดของพวกมัน ดังนั้นแม้ว่าเราจะไม่สามารถเขียน this<Exclude<T, undefined>> ได้ แต่เราสามารถเขียน Specify<this, Exclude<T, undefined>> และให้ผลลัพธ์เหมือนกัน
นี่คือตัวอย่างการใช้งานใหม่ของ MixedSchema:
declare class MixedSchema<T> {
type: T
validations: Validation[];
required(message?: string): Specify<this, Exclude<T, undefined>>;
oneOf<U extends T>(arrayOfValues: U[], message?: string):
Specify<this, U | Extract<undefined, T>>
}
และเราสามารถยืนยันได้ว่าตอนนี้มันทำงานได้เหมาะสมในซับคลาสแล้ว:
const s = new StringSchema() // StringSchema<string | undefined>
s.email(); // okay
const t = s.required(); // StringSchema<string>
t.email(); // okay
มาให้แน่ใจว่าประเภทถูกอนุมานตามที่คุณต้องการ:
const form = {
email: schema.string().required(),
age: schema.number().min(18),
gender: schema.string().oneOf(['male', 'female']).required(),
color: schema.string().oneOf(['red', 'blue', 'green']),
}
/* const form: {
email: StringSchema<string>;
age: NumberSchema<number | undefined>;
gender: StringSchema<"male" | "female">;
color: StringSchema<"red" | "blue" | "green" | undefined>;
} */
นั่นเป็นสัญญาณที่ดี; พารามิเตอร์ชนิดทั่วไปสำหรับแต่ละฟิลด์ถูกระบุอย่างถูกต้อง และดังนั้น InferType ของคุณควรจะสามารถดึงชนิดของฟิลด์เหล่านั้นได้:
type Form = InferType<typeof form>
/* type Form = {
email: string;
age: number | undefined;
gender: "male" | "female";
color: "red" | "blue" | "green" | undefined;
} */
ดูดี!
ลิงก์ Playground ไปยังโค้ด
