Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add undefined to tuple index signature, or reject keys that are not "keyof tuple" #38779

Open
5 tasks done
lonewarrior556 opened this issue May 26, 2020 · 5 comments
Open
5 tasks done
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@lonewarrior556
Copy link

lonewarrior556 commented May 26, 2020

Search Terms

Tuple index signature

Suggestion

I am opening up this issue as Seperate from #13778 since I am ok with the current interpretation of the Array.

declare var strArray: string[] // infinitely long list of type string
var a : strArray[10 as number]  // ok; a is string

but this fails in conjunction with tuples

declare var strTuple: [number, number] // length = 2
var a : strTuple[10 as number]  // absolutely not ok; a is string; 

Better Options:

var a : strTuple[10 as number] should either be string | undefined or it should give a type error similar to:

var a = { 0:  10, 1: 20} as const;
a[10 as number]; // type 'number' can't be used to index type

Tuples are already aware of boundaries and properly return undefined for more specific number types:

const t0: [string][0] = 'hello' // ok
const t1: [string][1] = 'hello' // Type '"hello"' is not assignable to type 'undefined'.

// therefore this is already true!
const t2: [string][0|1] // string | undefined 

why would type number act any differently?

Use Cases

// Easy fix for for loops

for (let i = 0 as 0 | 1; i < 2; i++) { 
  var definitly_string = strTuple[i] // type string
  var j = i + someNumber;
  var not_definitly_string  = strTuple[j] //lets you know you need to be careful
}

You could use keyof typeof strTuple instead of 0 | 1 when you fix this #27995

for (let i = 0 as keyof typeof strTuple; i < strTuple.length; i++) { 
  var definitly_string = strTuple[i] // type string
  var j = i + someNumber;
  var not_definitly_string = strTuple[j] //lets you know you need to be careful
} 

Given that strictNullChecks still allow typed arrays to do this:

var notString1 = strArray[Infinity]; // no error, not undefined
var notString2 = strArray[NaN]; // no error, not undefined

It would be nice to have a mechanism that could allow you to declare arrays with better type safety.

Examples

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Jun 4, 2020
@thw0rted
Copy link

thw0rted commented Jul 9, 2020

If I understand your issue correctly, you want [T,U,V][number] to type as T | U | V | undefined instead of just T | U | V. Is that right? I don't see how that's any different from / better than #13778.

This wouldn't be a breaking change in existing TypeScript/JavaScript code

This is not true.

declare const two: [string, string];
const oneOfthem = two[Math.floor(Math.random()*2)];
oneOfThem.charAt(0);

Before your proposed change, oneOfThem types as string, but after it would type as string | undefined. All the arguments that have been made against #13778 would then apply -- "but I checked that the index stays in-bounds!", etc.

@lonewarrior556
Copy link
Author

lonewarrior556 commented Jul 9, 2020

@thw0rted good points

If I understand your issue correctly, you want [T,U,V][number] to type as T | U | V | undefined instead of just T | U | V. Is that right?

     yes, either that or throw // type 'number' can't be used to index type)


I don't see how that's any different from / better than #13778.

     tuples were release much later and much more limited in use.. so unlike #13778 this would not be the single most breaking change in any project I've ever followed. meaning this is releasable.


This wouldn't be a breaking change in existing TypeScript/JavaScript code

This is not true.

     you are right, but it would be a minor breaking change, And honestly because the tuple [string] can accept [0 as const] but not [1 as const] means it cannot accept number so this is closer to a bug fix than feature


declare const two: [string, string];
const oneOfthem = two[Math.floor(Math.random()*2)];
oneOfThem.charAt(0);

     Unfortuanly TS does not know Math.floor(1 as const) is 1
     you expect it to?


Before your proposed change, oneOfThem types as string, but after it would type as string | undefined. All the arguments that have been made against #13778 would then apply -- "but I checked that the index stays in-bounds!", etc.

 // quick fix?
 const oneOfthem = two[Math.floor(Math.random()*2) as 1 | 2 ];

// general case
 const oneOfthem = two[Math.floor(Math.random() * two.length) as keyof two ];

@thw0rted
Copy link

thw0rted commented Jul 9, 2020

I still don't see this as a "minor" breaking change, but I'm not sure it's a big enough deal to merit a lot of debate. I make a point of not explicitly indexing arrays unless I absolutely have to -- I use for/of or forEach whenever possible -- so really, tuples should be no different. The large majority of existing tuple usage should be indexed with a literal number, right? But those cases already work, because the system is smart enough to treat tuple[1] as tuple[1 as const] rather than tuple[1 as number]. So already, this approach is trying to solve a problem that shouldn't happen often. Right?

@lonewarrior556
Copy link
Author

lonewarrior556 commented Jul 10, 2020

All I am saying is this

const t0: [string][0] = 'hello' // ok
const t1: [string][1] = 'hello' // Type '"hello"' is not assignable to type 'undefined'.

// therefore this is already true
const t2: [string][0|1] // string | undefined 

// How can you justify this behavior?
const t3: [string][number] // string 

Yes, for of/ forEach loops are better (although they still don't protect you from spares arrays)
But there are uses for using array indices

this was what brought me to the other thread:

var notString1 = aa[Infinity]; // no error, not undefined
var notString2 = aa[NaN]; // no error, not undefined

Which originally caused my runtime error as a number became a NaN, then returned an undefined from a tuple..
All of which was type safe

So objectively this is a problem that does happen.

@thw0rted
Copy link

I wonder if maybe what you need for your specific issue is actually #29317 ? If negation existed, you could declare your index as type FineiteNumber = number & (not Infinity) & (not -Infinity) & (not NaN), then e.g. parseInt would return number and you'd be reminded to type-guard down to FiniteNumber by checking for those edge cases before using it as a tuple index.

Obviously this does nothing for [string, string][5] but that's a different class of error and frankly you're a lot less likely to be in a position where the type checker could notice that it's a 5.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants