วิธีการประกาศ Array ความยาวคงที่ใน TypeScript


114

ในความเสี่ยงที่จะแสดงให้เห็นถึงการขาดความรู้เกี่ยวกับประเภท TypeScript ของฉัน - ฉันมีคำถามต่อไปนี้

เมื่อคุณทำการประกาศประเภทสำหรับอาร์เรย์เช่นนี้ ...

position: Array<number>;

... มันจะช่วยให้คุณสร้างอาร์เรย์ที่มีความยาวโดยพลการ อย่างไรก็ตามหากคุณต้องการอาร์เรย์ที่มีตัวเลขที่มีความยาวเฉพาะเช่น 3 สำหรับส่วนประกอบ x, y, z คุณสามารถสร้างประเภทสำหรับอาร์เรย์ที่มีความยาวคงที่ได้หรือไม่?

position: Array<3>

ขอความช่วยเหลือหรือชี้แจงใด ๆ ที่ชื่นชม!

คำตอบ:


181

อาร์เรย์จาวาสคริปต์มีตัวสร้างที่ยอมรับความยาวของอาร์เรย์:

let arr = new Array<number>(3);
console.log(arr); // [undefined × 3]

อย่างไรก็ตามนี่เป็นเพียงขนาดเริ่มต้นไม่มีข้อ จำกัด ในการเปลี่ยนแปลง:

arr.push(5);
console.log(arr); // [undefined × 3, 5]

typescript มีประเภททูเปิลที่ให้คุณกำหนดอาร์เรย์ที่มีความยาวและประเภทเฉพาะ

let arr: [number, number, number];

arr = [1, 2, 3]; // ok
arr = [1, 2]; // Type '[number, number]' is not assignable to type '[number, number, number]'
arr = [1, 2, "3"]; // Type '[number, number, string]' is not assignable to type '[number, number, number]'

22
ประเภททูเปิลจะตรวจสอบขนาดเริ่มต้นเท่านั้นดังนั้นคุณยังสามารถพุช "จำนวน" ได้ไม่ จำกัด จำนวนarrหลังจากเริ่มต้นแล้ว
benjaminz

4
จริงอยู่ที่มันยังคงใช้จาวาสคริปต์ที่รันไทม์เพื่อ "อะไรก็ได้" ณ จุดนั้น อย่างน้อยผู้ขนส่ง typescript จะบังคับใช้สิ่งนี้ในซอร์สโค้ดเป็นอย่างน้อย
henryJack

7
ในกรณีที่ฉันต้องการขนาดอาร์เรย์ขนาดใหญ่เช่น 50 มีวิธีระบุขนาดอาร์เรย์ด้วยชนิดที่ซ้ำกันหรือไม่เช่น[number[50]]เพื่อที่จะไม่ต้องเขียน[number, number, ... ]50 ครั้ง
Victor Zamanian

2
ไม่เป็นไรพบคำถามเกี่ยวกับเรื่องนี้ stackoverflow.com/questions/52489261/…
Victor Zamanian

1
@VictorZamanian เพียงเพื่อให้คุณทราบว่าแนวคิดที่จะตัดกัน{length: TLength}ไม่ได้ให้ข้อผิดพลาดในการพิมพ์หากTLengthคุณพิมพ์เกิน ฉันยังไม่พบไวยากรณ์ประเภทความยาว n ที่บังคับใช้ขนาด
Lucas Morgan

26

แนวทาง Tuple:

โซลูชันนี้มี ลายเซ็นประเภทFixedLengthArray (ak.a. SealedArray) ที่เข้มงวดตามใน Tuples

ตัวอย่างไวยากรณ์:

// Array containing 3 strings
let foo : FixedLengthArray<[string, string, string]> 

นี้เป็นวิธีที่ปลอดภัยที่สุดที่คิดว่ามันจะช่วยป้องกันการเข้าถึงดัชนีออกจากขอบเขต

การนำไปใช้:

type ArrayLengthMutationKeys = 'splice' | 'push' | 'pop' | 'shift' | 'unshift' | number
type ArrayItems<T extends Array<any>> = T extends Array<infer TItems> ? TItems : never
type FixedLengthArray<T extends any[]> =
  Pick<T, Exclude<keyof T, ArrayLengthMutationKeys>>
  & { [Symbol.iterator]: () => IterableIterator< ArrayItems<T> > }

การทดสอบ:

var myFixedLengthArray: FixedLengthArray< [string, string, string]>

// Array declaration tests
myFixedLengthArray = [ 'a', 'b', 'c' ]  // ✅ OK
myFixedLengthArray = [ 'a', 'b', 123 ]  // ✅ TYPE ERROR
myFixedLengthArray = [ 'a' ]            // ✅ LENGTH ERROR
myFixedLengthArray = [ 'a', 'b' ]       // ✅ LENGTH ERROR

// Index assignment tests 
myFixedLengthArray[1] = 'foo'           // ✅ OK
myFixedLengthArray[1000] = 'foo'        // ✅ INVALID INDEX ERROR

// Methods that mutate array length
myFixedLengthArray.push('foo')          // ✅ MISSING METHOD ERROR
myFixedLengthArray.pop()                // ✅ MISSING METHOD ERROR

// Direct length manipulation
myFixedLengthArray.length = 123         // ✅ READ-ONLY ERROR

// Destructuring
var [ a ] = myFixedLengthArray          // ✅ OK
var [ a, b ] = myFixedLengthArray       // ✅ OK
var [ a, b, c ] = myFixedLengthArray    // ✅ OK
var [ a, b, c, d ] = myFixedLengthArray // ✅ INVALID INDEX ERROR

(*) โซลูชันนี้ต้องเปิดใช้คำสั่งการกำหนดค่าnoImplicitAny typescript เพื่อให้ทำงานได้ (แนวทางปฏิบัติที่แนะนำโดยทั่วไป)


แนวทาง Array (ish):

โซลูชันนี้ทำงานเป็นการเพิ่มArrayประเภทโดยยอมรับพารามิเตอร์ที่สองเพิ่มเติม (ความยาวอาร์เรย์) ไม่เป็นที่เข้มงวดและปลอดภัยเป็นวิธีการแก้ปัญหา Tuple ตาม

ตัวอย่างไวยากรณ์:

let foo: FixedLengthArray<string, 3> 

โปรดทราบว่าวิธีการนี้จะไม่ป้องกันไม่ให้คุณเข้าถึงดัชนีนอกขอบเขตที่ประกาศไว้และกำหนดค่าไว้

การนำไปใช้:

type ArrayLengthMutationKeys = 'splice' | 'push' | 'pop' | 'shift' |  'unshift'
type FixedLengthArray<T, L extends number, TObj = [T, ...Array<T>]> =
  Pick<TObj, Exclude<keyof TObj, ArrayLengthMutationKeys>>
  & {
    readonly length: L 
    [ I : number ] : T
    [Symbol.iterator]: () => IterableIterator<T>   
  }

การทดสอบ:

var myFixedLengthArray: FixedLengthArray<string,3>

// Array declaration tests
myFixedLengthArray = [ 'a', 'b', 'c' ]  // ✅ OK
myFixedLengthArray = [ 'a', 'b', 123 ]  // ✅ TYPE ERROR
myFixedLengthArray = [ 'a' ]            // ✅ LENGTH ERROR
myFixedLengthArray = [ 'a', 'b' ]       // ✅ LENGTH ERROR

// Index assignment tests 
myFixedLengthArray[1] = 'foo'           // ✅ OK
myFixedLengthArray[1000] = 'foo'        // ❌ SHOULD FAIL

// Methods that mutate array length
myFixedLengthArray.push('foo')          // ✅ MISSING METHOD ERROR
myFixedLengthArray.pop()                // ✅ MISSING METHOD ERROR

// Direct length manipulation
myFixedLengthArray.length = 123         // ✅ READ-ONLY ERROR

// Destructuring
var [ a ] = myFixedLengthArray          // ✅ OK
var [ a, b ] = myFixedLengthArray       // ✅ OK
var [ a, b, c ] = myFixedLengthArray    // ✅ OK
var [ a, b, c, d ] = myFixedLengthArray // ❌ SHOULD FAIL

1
ขอบคุณ! อย่างไรก็ตามยังคงเป็นไปได้ที่จะเปลี่ยนขนาดของอาร์เรย์โดยไม่ได้รับข้อผิดพลาด
Eduard

1
var myStringsArray: FixedLengthArray<string, 2> = [ "a", "b" ] // LENGTH ERRORดูเหมือนว่า 2 ควรเป็น 3 ที่นี่?
Qwertiy

ฉันได้อัปเดตการใช้งานด้วยโซลูชันที่เข้มงวดขึ้นซึ่งป้องกันการเปลี่ยนแปลงความยาวอาร์เรย์
colxi

@colxi เป็นไปได้หรือไม่ที่จะมีการใช้งานที่อนุญาตให้ทำการแมปจาก FixedLengthArray ไปยัง FixedLengthArray อื่น ๆ ตัวอย่างสิ่งที่ฉันหมายถึง:const threeNumbers: FixedLengthArray<[number, number, number]> = [1, 2, 3]; const doubledThreeNumbers: FixedLengthArray<[number, number, number]> = threeNumbers.map((a: number): number => a * 2);
Alex Malcolm

@AlexMalcolm ฉันกลัวว่าmapจะให้ลายเซ็นอาร์เรย์ทั่วไปไปยังเอาต์พุต ในกรณีของคุณมักจะเป็นnumber[]ประเภท
colxi

11

จริงๆแล้วคุณสามารถทำได้ด้วย typescript ปัจจุบัน:

type Grow<T, A extends Array<T>> = ((x: T, ...xs: A) => void) extends ((...a: infer X) => void) ? X : never;
type GrowToSize<T, A extends Array<T>, N extends number> = { 0: A, 1: GrowToSize<T, Grow<T, A>, N> }[A['length'] extends N ? 0 : 1];

export type FixedArray<T, N extends number> = GrowToSize<T, [], N>;

ตัวอย่าง:

// OK
const fixedArr3: FixedArray<string, 3> = ['a', 'b', 'c'];

// Error:
// Type '[string, string, string]' is not assignable to type '[string, string]'.
//   Types of property 'length' are incompatible.
//     Type '3' is not assignable to type '2'.ts(2322)
const fixedArr2: FixedArray<string, 2> = ['a', 'b', 'c'];

// Error:
// Property '3' is missing in type '[string, string, string]' but required in type 
// '[string, string, string, string]'.ts(2741)
const fixedArr4: FixedArray<string, 4> = ['a', 'b', 'c'];

แก้ไข (หลังจากผ่านไปนาน)

สิ่งนี้ควรจัดการกับขนาดที่ใหญ่กว่า (โดยพื้นฐานแล้วมันจะขยายอาร์เรย์แบบทวีคูณจนกว่าเราจะได้พลังใกล้เคียงที่สุดของสอง):

type Shift<A extends Array<any>> = ((...args: A) => void) extends ((...args: [A[0], ...infer R]) => void) ? R : never;

type GrowExpRev<A extends Array<any>, N extends number, P extends Array<Array<any>>> = A['length'] extends N ? A : {
  0: GrowExpRev<[...A, ...P[0]], N, P>,
  1: GrowExpRev<A, N, Shift<P>>
}[[...A, ...P[0]][N] extends undefined ? 0 : 1];

type GrowExp<A extends Array<any>, N extends number, P extends Array<Array<any>>> = A['length'] extends N ? A : {
  0: GrowExp<[...A, ...A], N, [A, ...P]>,
  1: GrowExpRev<A, N, P>
}[[...A, ...A][N] extends undefined ? 0 : 1];

export type FixedSizeArray<T, N extends number> = N extends 0 ? [] : N extends 1 ? [T] : GrowExp<[T, T], N, [[T]]>;

2
ฉันจะใช้สิ่งนี้ได้อย่างไรเมื่อจำนวนองค์ประกอบเป็นตัวแปร ถ้าฉันมี N เป็นประเภทตัวเลขและ "num" เป็นตัวเลขดังนั้น const arr: FixedArray <number, N> = Array.from (new Array (num), (x, i) => i); ให้ฉัน "การสร้างอินสแตนซ์ประเภทลึกเกินไปและอาจไม่มีที่สิ้นสุด"
Micha Schwab

2
@MichaSchwab น่าเสียดายที่ดูเหมือนว่าจะใช้ได้กับตัวเลขที่ค่อนข้างเล็กเท่านั้น มิฉะนั้นจะบอกว่า "การเรียกซ้ำมากเกินไป" เช่นเดียวกันกับกรณีของคุณ ฉันไม่ได้ทดสอบอย่างละเอียด :(
Tomasz Gawel

ขอบคุณที่ติดต่อกลับมา! หากคุณพบวิธีแก้ปัญหาสำหรับความยาวผันแปรโปรดแจ้งให้เราทราบ
Micha Schwab

1
@MichaSchwab ดูคำตอบที่อัปเดต
Tomasz Gawel
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.