#How to assign object to Record<string, unknown>?

519 messages ยท Page 1 of 1 (latest)

idle crownBOT
#
onkeltem#0

Preview:```ts
declare function foo(
arg: Record<string, unknown>
): void

function bar(v: unknown) {
if (typeof v === "object" && v != null) {
foo(v)
}
}```

strong cave
#
Argument of type 'object' is not assignable to parameter of type 'Record<string, unknown>'.
  Index signature for type 'string' is missing in type '{}'.(2345)
#

Honestly, I stumble upon this too often but cannot remember how the heck it's not working

amber trellis
#

a Record<string, unknown> can have any value read from or written to any key
the index signature allows that, so if you lack an index signature, that isn't assignable

strong cave
craggy cipher
#

no:

idle crownBOT
#
declare const x: object
x.arbitraryKey = 'value'
//^^^^^^^^^^^^
// Property 'arbitraryKey' does not exist on type 'object'.
amber trellis
#

oh come on that's the second time i got ninja'd with nearly the same content today

craggy cipher
#

๐Ÿฅท

strong cave
#

foo() is a function that makes hash for example

#

it needs any object with string keys, and it just converts it to json and takes hash

#

Record<string, unknown> seems to be a good option

#

But obviously it's not

#

Record<string, unknown> => {} - works

#

but then eslint is starting to bark

#

Here is anothe example:

idle crownBOT
#
onkeltem#0

Preview:```ts
function sortObjectKeys(
obj: Record<string, unknown>
): Record<string, unknown> {
const sortedObj: Record<string, unknown> = {}

Object.keys(obj)
.sort()
.forEach(key => {
const v = obj[key]
if (
typeof v === "object" &&
v != null &&
!Array.isArray(v)
...```

strong cave
#

Problem is the same

#

I cannot recursively call the function

amber trellis
#

do you need really need Record<string, unknown> there though?

strong cave
#

no, absolutely not. I just needed obj[key] to work

#

but seems like I cannot achieve that w/o the Record

craggy cipher
#

seems like the core thing is just how to narrow unknown to Record<string, unknown>. you'll need a type predicate for that

#

for example:

idle crownBOT
#
mkantor#0

Preview:```ts
function isUnknownRecord(
v: unknown
): v is Record<PropertyKey, unknown> {
return (
typeof v === "object" &&
v != null &&
!Array.isArray(v)
)
}

function sortObjectKeys(
obj: Record<string, unknown>
): Record<string, unknown> {
const sortedObj: Record<string, unknow
...```

craggy cipher
#

stepping back though i would generally advise to not rely on implicit ordering of object properties

#

i'd probably put them in an array or implement my own explicitly-ordered iterator or something instead

strong cave
#

you think it can be not enough?

#

then I calculate hash from that thing. I don't send it via network or something

strong cave
craggy cipher
#

ah, the usecase is to generate a stable hash of an object? if so i wouldn't DIY it... 1 sec let me find a library i've used for this before

wooden silo
#

Are there things that are of type object (other than null) that you can't do it for?

craggy cipher
#

@wooden silo maybe start a new help thread? it's a bit odd to message in someone else's 2.5-year-old help thread about something only tangentially-related

wooden silo
#

I didn't want to duplicate the thread, I was encouraged to search for existing. But sure, I can create a new one, no problem. Sorry.

amber trellis
craggy cipher
#

yeah, and given โ˜๏ธ the answer is "yes":

const x: {} = 'blah'
const y: object = x
y.thisWillBlowUpAtRuntime = 'oops'
wooden silo
#

Doesn't blow up.

craggy cipher
#

object in TS is a footgun; it doesn't really mean "my typeof is 'object'" like the other lowercased built-in types do

wooden silo
#

It should tho.

amber trellis
craggy cipher
wooden silo
#

If I want to use TS for regular JS, that's not suitable now?

amber trellis
#

typescript disallows a ton of stuff that's allowed in js, that's the point

craggy cipher
#

also, strict mode is part of "regular JS"

wooden silo
#

Fine: "typescript doesn't make sense without strict mode"?

craggy cipher
#

also also nothing i said implied that it only makes sense for that?

wooden silo
amber trellis
#

and the case mkantor presented, while it doesn't error, also doesn't make sense. y.thisWillBlowUpAtRuntime is still nonexistant/undefined, since you tried to assign to a primitive

wooden silo
wooden silo
amber trellis
#

you can at runtime. ts will complain.

craggy cipher
wooden silo
wooden silo
craggy cipher
#

42 === 'blah' is also a typescript error, for example

craggy cipher
wooden silo
amber trellis
amber trellis
wooden silo
#

like doing that:

const obj = {};
obj.key = "value";

I see nothing wrong with that, I think TS should allow that.

craggy cipher
#

if you want to do that, typescript lets you (just add an index signature)

#

but it's a good thing that it has to be explicit

amber trellis
craggy cipher
#

as most values should not be expando, IMO

#

at least in typical programs

wooden silo
#

And I'm struguling to write sound-ts for that.

amber trellis
#

you can do that within the type system

#

ts inherently isn't very sound lol

#

any of course, but also void, object, upcasting, optionals, tuples, rest/spread, etc

wooden silo
#

If that value: uknown is an object that has fields start and end, both of which are number, I want to return them. In any other case I want to throw an error.

#

And you can see, ts shows me an error.

craggy cipher
#

oh hey are you the person from this issue? i didn't recognize the username similarity. that's good context ๐Ÿ˜„

wooden silo
#

And I know I can silence these errors in a lot of ways, using // @ts-ignore, using any, using as, using type assertion in but these don't check anything.

amber trellis
#

this should definitely be its own thread now lol

#

should've been a while ago. mb

wooden silo
#

The problem I have with as and in, is that you can put anything there and ts doesn't check it. there are no type guards there.

#

So If I make a mistake, ts will not check it.

amber trellis
#

(do you mean is)

wooden silo
#

People have said: "well you're supposed to write it correctly", like what does that mean.

wooden silo
amber trellis
#

that would be as?

amber trellis
wooden silo
wooden silo
craggy cipher
wooden silo
#

Either way, all of the silence typescript errors.

wooden silo
amber trellis
#

and yeah, as/<T>x/is/asserts..is don't check by design because they're for when you can't (or won't) prove something within the ts type system

wooden silo
amber trellis
amber trellis
wooden silo
#

Had TS not have type-guards it wouldn't be possible.

#

But type-guards will allow me to dynamically-type-check it without using as.

#

One hole I fell into was thinking that typeof x === 'object' and object are the same ๐Ÿ˜‚ They're not.

amber trellis
#

field in value where field: string can't narrow value, as a design limitation of ts

wooden silo
#

I suppose it can narrow it down to string|symbol|number tho?

#

only those can be in value, right?

amber trellis
#

this should be in a different thread.

#

this convo can go much farther lol

craggy cipher
#

!validation-libs

wooden silo
#

No, I don't want that.

craggy cipher
#

!:validation-libs

#

er i forget the bot command syntax, 1 sec

wooden silo
#

I want to write typescript myself, I don't want to add a needless dependency for something so simple.

craggy cipher
#

oh just had the name wrong

wooden silo
#

I don't want any library for that. As far as I'm concernred they might be using as too.

craggy cipher
#

!:validation-libraries

idle crownBOT
wooden silo
#

I just want to read a field from an object if it exists! I shouldn't need a whole library for that.

craggy cipher
wooden silo
#

Maybe, if that's the case then I'll use a library.

craggy cipher
#

if you're sure you only need a flat thing and no custom types, etc then sure, DIY it

wooden silo
#

But for now I just accept an arbitrary value, and an arbitrary string field name, and if that value has that field, return it, if not throw.

amber trellis
wooden silo
#

Guys, thank you for your concern, but I know what I'm doing. I don't need a library at this point, just a simple ts function.

#

I have that, but it complains about ts errors:

function returnFieldValue(value: unknown, field: string): number {
  if (typeof value === 'object' && value !== null) {
    if (field in value) {
      const valueElement = value[field];
      if (typeof valueElement === 'number') {
        return valueElement;
      }
    }
  }
  throw new Error('Failed to cast notification payload.');
}
amber trellis
#

right, because ts can't narrow value

craggy cipher
craggy cipher
#

yeah, and you posted a follow-up comment (now deleted) saying that worked for your use case, right?

#

is there something missing?

wooden silo
#
function returnFieldValue(value: unknown, field: string): number {
  if (typeof value === 'object' && value !== null) {
    if (field in value) {
      const valueElement: Partial<Record<PropertyKey, unknown>> = value[field];
      if (typeof valueElement === 'number') {
        return valueElement;
      }
    }
  }
  throw new Error('Failed to cast notification payload.');
}

@craggy cipher Should that work?

amber trellis
#

well it does also use unsound stuff technically lol

craggy cipher
#

you can also generalize the value type without too much complexity, FWIW. i need to step away for a bit but i can share an example if you'd like

wooden silo
craggy cipher
#

optional properties are one, like i said in that issue

amber trellis
wooden silo
#

You can use any js checking in runtime and all ts type guards; but I don't want as or anything that silences the type checker.

amber trellis
amber trellis
#

is is technically is not an assertion, it is used to form a predicate

wooden silo
#

You mean js is? Oh yes, I can use that because it actually runs and checks the type.

amber trellis
#

assertions are type assertions <T>x and x as T, and nonnullish assertion x!

wooden silo
#

Sorry, I ment in ๐Ÿ˜„

amber trellis
wooden silo
#

x! might return null in runtime

amber trellis
#

yes, hence why i mentioned them in the list of unsound things

wooden silo
#

Oh, that was a list of unsound items. I thought it was a list of sound items.

#

Okay, yes! That's good.

#

I don't want to use any of them.

amber trellis
#

there are more that i haven't included because ive forgotten or i don't know them, probably

#

oh index signatures too

wooden silo
#
function returnFieldValue(value: unknown, field: string): unknown {
  if (typeof value === 'object' && value !== null) {
    if (Symbol.iterator in value) {
      if (typeof value[Symbol.iterator] === 'function') {
        for (const existingField of value) {
          if (existingField === field) {
            return field;
          }
        }
      }
    }
  }
  throw new Error('Failed to cast notification payload.');
}

I tried that.

#

Instead of reading a value by index, maybe I could just iterate and compare.

#

But that complains about the value[Symbol.iterator] returning not an iterator.

amber trellis
wooden silo
#

I'm okay with void, because I will not write code that does anything to it.

amber trellis
#

that's kinda the thing with all the rest too

wooden silo
#

So even if something is put into void,I don't care about that.

#

I'm interested in things that can sneak in and propagate to the rest of the system.

amber trellis
#

all of them, including void, kinda are

wooden silo
#

like

function cheat(): number{
  return "haha!" as number;
}
amber trellis
#

that's kinda what unsoundness is about

wooden silo
#

Reading a field from an object shouldn't be like that.

magic stone
#

Part of the issue with some of this is that field is string - TS can't indicate by the type system that you've checked that field is in value.

wooden silo
#

It should be possible to just read a field from an object.

magic stone
#

This is the same snippet as before, except field has been moved from an arbitrary string to a specific string, and it compiles fine:

function returnFieldValue(value: unknown): number {
  const field = "someSpecificString"
  if (typeof value === 'object' && value !== null) {
    if (field in value) {
      const valueElement = value[field];
      if (typeof valueElement === 'number') {
        return valueElement;
      }
    }
  }
  throw new Error('Failed to cast notification payload.');
}
amber trellis
wooden silo
#

Yea, specific string works fine.

#

I need to do it for arbitrary string.

craggy cipher
#

which you can do without any assertions

amber trellis
wooden silo
craggy cipher
wooden silo
#

I'm fine with the property being unknown.

wooden silo
craggy cipher
wooden silo
#
test('test', () => {
  assertEquals('string', cast({foo: 2}, 'foo')); // returns 2
});

function cast(value: unknown, field: PropertyKey): string {
  if (value !== null && value !== undefined && typeof value === 'object') {
    if (field in value) {
      const possiblyHasField: Partial<Record<PropertyKey, string>> = value;
      let possiblyHasFieldElement = possiblyHasField[field];
      if (possiblyHasFieldElement !== undefined) {
        return possiblyHasFieldElement;
      }
    }
  }
  throw new Error();
}

How is this allowed? :/

#

Typescript should protect us from misusing types, and here it is, it allowed me to do the wrong thing.

amber trellis
#

also classes as a whole are unsound because you can declaration merge into their interface

craggy cipher
# wooden silo yes, please do.

i'm not sure it's possible to do this without a type predicate (is). the thing i was thinking of won't work because typescript won't narrow x in typeof x === y when y is a variable. that only happens when the right-hand side of the === is a literal string

#

but its easy to write an obviously-safe type guard for this that uses is

amber trellis
craggy cipher
#

which, as with all of this unsafe/unsound stuff, is the best you can do (contain the unsoundness behind a safe/tested API)

wooden silo
#

@craggy cipher I managed to do something like that:

function region(value: unknown): Region {
  if (typeof value === 'object' && value !== null && 'start' in value && 'end' in value) {
    return {
      start: requireNumber(value['start']),
      end: requireNumber(value['end']),
    };
  }
  throw new Error('Failed to cast notification payload.');
}

function requireNumber(value: unknown): number {
  if (typeof value === 'number') {
    return value;
  }
  throw new Error('Failed to cast notification payload.');
}
#

Would you say this is sound?

craggy cipher
# wooden silo Would you say this is sound?

as and is can be used to write sound code too via proper encapsulation. if you're asking if this code uses any language features which are potentially unsound then the answer is no (in for example is risky)

magic stone
#

Here's realistically how I'd write this:

function hasKey<K extends PropertyKey>(obj: object, key: K): obj is Record<K, unknown> {
    return key in obj;
}

function returnFieldValue<K extends string>(value: unknown, field: K): number {
  if (typeof value === 'object' && value !== null) {
    if(hasKey(value, field)) {
      const valueElement = value[field];
      if (typeof valueElement === 'number') {
        return valueElement;
      }
    }
  }
  throw new Error('Failed to cast notification payload.');
}

And yeah, hasKey is technically using unsound features but it's reasonably safe.

amber trellis
#

you could argue that narrowing properties is unsound too, because of getters

wooden silo
#

That hasKey() is essentially the same thing as putting as in there.

#

value as Record<PropertyKey, unknown.

#

Achieves the same result.

amber trellis
#

it's different in maintainability

craggy cipher
magic stone
#

Except that by putting it in a utility function, you can verify it once, rather than having to audit every use of as individually for correctness.

amber trellis
wooden silo
magic stone
#

You're probably not going to find anything that meets your standards.

craggy cipher
wooden silo
magic stone
#

Typescript is built on Javascript, it's intentionally not a sound language and probably could never be.

amber trellis
magic stone
#

The key is just to be pragmatic about where things get 'unsafe'.

wooden silo
wooden silo
#

But I use in only for that one property, that should be safe.

amber trellis
wooden silo
craggy cipher
wooden silo
amber trellis
# wooden silo wdym?
function getProp(obj: object, key: string) {
  return Object.entries(obj).find(([k, v]) => k === key)?.[1] ?? null //or whatwver
}

but this is O(n) and allocates an array, both of which aren't issues for direct object access

wooden silo
#

Is there any js element that matches typeof x === 'object' and yet cannot be used as Record<string, unkown>? ๐Ÿค”

amber trellis
#

well, null

wooden silo
#

Yea, other than null.

amber trellis
#

depends on what you mean by "cannot be used as" though

wooden silo
#

The reason why ts doesn't allow you to assign string to number is because you can do things to one but not the other, right?

amber trellis
#

correct

wooden silo
#

So is there anything that I can do to object that I can't to Record<PropertyKey,unknown> or vice-verca?

amber trellis
#

is string a valid Record<PropertyKey, unknown>, in your mind? not what ts allows in assignability, but whether you think that makes sense

craggy cipher
wooden silo
#

string is not typeof str === 'object', and I don't thing string is ts object either.

amber trellis
#

Record<PropertyKey, V> generates index signatures, and index signatures are 3 constraints in one

amber trellis
#

because ts object is unsound

craggy cipher
#

in that case, everything except null or undefined is a Record<PropertyKey, unknown> in javascript

wooden silo
#

Or Exclude<object, null> to Record<PropertyKey, unknown>

amber trellis
#

that wouldn't be narrowing, but that aside, because ts doesn't implement that

craggy cipher
#

same reason 42 === 'blah' is an error, as we talked about before

amber trellis
#

whatever check you would use to "narrow" that case would also be used in other cases, where Record<PropertyKey would be much too broad to be useful in those other cases

wooden silo
#

But I digress.

craggy cipher
#

'wowie'.something is valid javascript, in case that's not clear. and 'wowie'.something.somethingelse.oops.jeez is also "valid" javascript in that it is well-specified what its runtime behavior is

wooden silo
#

I would like to write a function that takes unknown, and returns value from it or throws, but in a way that I don't need to silence ts checks.

craggy cipher
wooden silo
#

'wowie'.something.somethingelse.oops.jeez that would fail, because 'wowie'.something is probably undefined.

#

But I don't want to get into that.

#

I just want a way I can use typescript type guards to read the field from an object.

#

but type guards, not type assertions.

amber trellis
#

do you mean narrowing

#

type guards/predicates are what is makes, they're unsound

wooden silo
#

aren't these called type guards?

wooden silo
#

The same thing?

amber trellis
#

x as T and <T>x

#

as ive mentioned previously...

wooden silo
#

I mean things like if (typeof x === 'object') or if (x in obj)

amber trellis
#

that's narrowing

wooden silo
#

I thought they were called type guards, but fine, we can call them narrowing.

craggy cipher
amber trellis
#

it could be that type guards are used to refer to both

#

yeah there we go lmao

craggy cipher
#

but yeah some people also use the terms more specifically. it's confusing

amber trellis
#

fun times

wooden silo
#

I have that:

#
function region(value: unknown): Region {
  if (typeof value === 'object' && value !== null && 'start' in value && 'end' in value) {
    return {
      start: requireNumber(value['start']),
      end: requireNumber(value['end']),
    };
  }
  throw new Error('Failed to cast notification payload.');
}

function requireNumber(value: unknown): number {
  if (typeof value === 'number') {
    return value;
  }
  throw new Error('Failed to cast notification payload.');
}
#

But I would like to use it for arbitrary fields, not just defined ones.

amber trellis
#

what answer are you expecting at this point

#

you've been given multiple answers

wooden silo
#

How to write that?

#

Either with as, that sucks

#

Or with type assertion in return, that also sucks

amber trellis
#

retsam already answered this

wooden silo
#

Or with Object.entires() that's O(n)

#

Or with Partial<> that also sucks.

amber trellis
#

there is no answer that doesn't suck in your definition here

wooden silo
#

Yea, I don't think so. I think there is a way, I just need to search more for it.

amber trellis
#

there is one that sucks the least, which i would argue is retsam's. it's clear about the unsafety and encapsulated in a way that's easy to audit

wooden silo
#

Either I'm right, and there is a way to do it, and there is a way in ts to do it, or it's a limitation of ts.

#

Or I'm not right, and there are some wierd cases, but at least they'll be illustrated to me.

#

right now I believe what I want should be doable, but it appears that it's exactly clear how to do it in TS.

amber trellis
#

it's a limitation of ts.

wooden silo
amber trellis
#

there's a ton of things that people think ts should add

#

manpower and motivation is finite

craggy cipher
#

also most of this stuff is a direct consequence of the intentional design of TS. it's not like a mistake or something

wooden silo
#

Either it's the correct behaviour, and they should add it;
or it's incorrect behaviour and I'm missing something - if that's the case, what am I missing?

amber trellis
#

that ts isn't designed for it

craggy cipher
#

you want TS to be A but it is actually B

wooden silo
#

But it can narrow certain types, right?

#

So how come narrowing to number works fine, but narrowing to object is wierd all of a sudden?

amber trellis
#

ts choose to not do some things that would be useful, because it would be disproportionately much work

wooden silo
#

I didn't invent narrowing, it's TS's invention.

#

If it didn't have narrowing at all, I'd be fine with as.

#

Because I would understand there's just no other way.

amber trellis
wooden silo
#

But because it introduced narrowing, it should work for numbers, string and objects too.

amber trellis
#

it can narrow to object just fine. it doesn't narrow to { [k: field]: unknown }.

wooden silo
#

That leads me to believe that narrowing is a thorough feature in TS, and I just can't use it properly yet.

wooden silo
craggy cipher
wooden silo
#

Apart from null.

craggy cipher
wooden silo
amber trellis
amber trellis
craggy cipher
wooden silo
amber trellis
wooden silo
amber trellis
#

if you want a type that allows arbitrary property access, that'd be Record<PropertyKey, unknown>.

#

ts doesn't synthesize index signatures from narrowing because that's a semantic thing that you can't get from narrowing.

wooden silo
#

I'm of the opinion, that just calling field in object should be enough to dedeuce whether I can access object[field].

craggy cipher
amber trellis
wooden silo
amber trellis
#

have you been reading half my messages

wooden silo
#

Calling field in obj should narrow something, right?

#

But it behaves as if it does nothing.

amber trellis
#

i should be more specific

amber trellis
subtle basin
#

If you previously have code:

type User = {
    foo: string
}

function fn(user: User) {
    console.log(user.foo)
}

And during refactoring:

type User = {
-   foo: string
+   bar: string
}

But you forgot to update the corresponding code in fn.
With the current TS behavior, you will immediately get a compile error because user.foo is no longer accessible. With your proposed behavior, then that would pass type checking and potentially silently cause runtime logical errors.

wooden silo
#

With the current TS behavior, you will immediately get a compile error because user.foo is no longer accessible.
yes, and that's a very cool feature of TS.

#

With your proposed behavior, then that would pass type checking and potentially silently cause runtime logical errors.
How so? I don't need to allow any kind of field on any object.

#

But, I see what you mean.

#
type User = {
    foo: string
}

function fn(user: User) {
  if ('bar' in user) {
    console.log(user.bar); // should this be allowed?
  }
  user.bar; // this shouldn't be allowed, definitely
}
idle crownBOT
#
type User = {
    foo: string
}

function fn(user: User) {
  if ('bar' in user) {
    console.log(user.bar); // should this be allowed?
  }
  user.bar; // this shouldn't be allowed, definitely
//     ^^^
// Property 'bar' does not exist on type 'User'.
}
amber trellis
#

it is allowed

subtle basin
#

It is allowed in TS currently, because people do write that kind of code.

amber trellis
#

i'm confused why this is a point of contention

wooden silo
#

I don't know, @subtle basin suggested it.

amber trellis
#

they didn't though...?

wooden silo
#

If I have object, and I check field in object I should be able to do object[field]? It's always going to work in js, and as far as I can tell in TS, too, so why can't it work?

#

Is there some other piece of code that would break?

amber trellis
#

the ts codebase, yes

amber trellis
wooden silo
#

Can you show me what TS code would break if in worked the way I described it?

subtle basin
#

There's a huge difference between "checking 'bar' in user first, then accessing user.bar" vs "just access user.bar"
The former is very clear that you are intentionally checking for bar and thus after the successful check, user.bar should be accessible. The latter could very well be a mistake, eg a typo of user.baz.

wooden silo
#

Not what I meant.

amber trellis
#

that is what i'm saying though

#

typescript doesn't implement this because it would be too much work

#

not because it's incorrect

wooden silo
#

I mean, is there some client typescript code that would be rendered contradictory if in worked like I described?

amber trellis
#

ive said this multiple times

amber trellis
wooden silo
#

Because what I'm hearing you saying is what Im describing is probably correct, but it's hard to introduce that behaviour into the compiler?

amber trellis
#

yes

magic stone
#

It's also a trade-off against other factors.

wooden silo
magic stone
#

I'm not fully following, but if you mean "I should be able to access any property without proving it exists and just narrow it later" - that can make it easier for typos to slip through

wooden silo
#

No, I want to first prove it exists and then access it.

magic stone
#

Oh, you still mean with a string typed value?

#

Yeah, that's still "probably safe, but there's no way for the type system to represent it".

wooden silo
#

I want to do that:

const field: string = 'myField'; 
const obj: object = {};
if (field in obj) {
  obj[field]; // i want that
}
magic stone
#
const obj: object = {};
if (field in obj) {
  // what is the type of obj here that would make the next line work?
  obj[field]; // i want that
}
wooden silo
#

I suppose a lot, could be Record<string,unknown>, Record<PropertyKey,unknown>, { [key: PropertyKey]: unknown }, I don't care really.

magic stone
#

obj needs to have some inferred type inside the if and if field is string, there's just no type you can put there that makes it work

magic stone
wooden silo
#

oh,,, yes you're right ๐Ÿ˜ฎ

amber trellis
wooden silo
#

Ooooooooooooooooh, kay. I'm starting to understand this now.

#

@magic stone you're right.

subtle basin
# wooden silo If I have `object`, and I check `field in object` I should be able to do `object...

Keep in mind that a literal like 'foo' in obj already narrows, and it works by narrowing obj to Record<'foo', unknown>.
The reason why key in obj (where key is a variable) doesn't narrow is because of cases where key is an infinite set, such as string. If TS applied the same logic, then obj would be narrowed to Record<string, unknown>, which would allow the following code to pass type checking:

declare const key: string
declare const obj: object

if (key in obj) {
    // This would be fine because we already checked key's existence
    obj[key]

    // This would also be allowed because obj is now narrowed to Record<string, unknown>
    // But you probably don't want to allow this
    obj.literallyAnyString
}
wooden silo
#

Doesn't have to be an infinte set, same logic applies to that:

const field: 'a'|'b' = 'myField'; 
const obj: object = {};
if (field in obj) {
  obj[field]; // allows either a or b, but object might contain only a or b
}
#

but you see, here'e the catch.

#
const field: string;
const somethingElse: string;

if (field in obj) {
  obj[field]; // that should work
  obj[somethingElse]; // that shouldn't
} 
#

Yes, both field and somethingElse are the same type, but not the same variable.

subtle basin
#

That example doesn't matter because field is const, so obj[field] is fully sound.
Rather the issue is that you would be able to access both obj.a and obj.b, even though you have only proved one of the key exists.

wooden silo
#

I think I got it.

wooden silo
subtle basin
#

I'm just explaining why the current mechanism of in narrowing in TS (by narrowing to Record<K, unknown>) doesn't work for the case where lhs is not a literal.
If you want it to work, then you would have to propose some different mechanism of how TS's in narrowing needs to work.

magic stone
amber trellis
amber trellis
wooden silo
#

Well, if typescript downgrades all value checks to how their types are declared, then it's hopeless :/

magic stone
#

Looking for perfectly sound code in TS is probably hopeless yes.

#

It can be a perfectly useful, pragmatic language.

wooden silo
magic stone
#

It'd be complicated and fragile

amber trellis
#

because it'd be a lot of work for a small usecase

#

would probably need a lot of changes to the typechecker

wooden silo
magic stone
#

This would be the main use-case.

amber trellis
#

oh, yeah it'd be fine if implemented fully

#

probably also some stuff with switches

magic stone
#

And it assumes key is immutable (and function params aren't!)

craggy cipher
#

i could imagine a language feature that synthesizes type parameters within specific scopes to make something like ref key work (and i think it'd be a pretty cool language feature), but it's not simple and has limited utility (i guess i'm basically agreeing with what was said above)

amber trellis
#

consider the complexity introduced lol

function f(obj: { a: unknown }) { console.log(obj.a); }

function g(obj: object, key: string) {
  if (key in obj) {
    // obj: Record<ref key, unknown>
    f(obj) // invalid
    if (key === "a") {
      f(obj) // valid now? but obj wasn't narrowed
    }
  }
}
wooden silo
#

In other words key in obj necessarily implies key === "a".

amber trellis
#

because key in obj would only be true if that key was actually a
...no?

#

that's just not true

wooden silo
#

What other key would make that condition true?

amber trellis
#

any key that's in obj

#

obj in g doesn't have any specified keys

#

and even if it did specify a, other keys could exist

wooden silo
#

It would seem other values just don't get executed.

wooden silo
#

Ah, found a use-case:

wooden silo
#

So I would say that's right, checks on other values can narrow now the value in question. I find it very reasonable.

magic stone
#

A function call that's invalid, then you do a check on some variable that isn't used in the function call, then that check somehow makes the function call valid is pretty complicated.

#

You'd need pretty sophisticated (and probably imperfect) type-system machinery to represent that.

#

Realistically this isn't going to change. Even if you can theorycraft some type-system mechanism by which a type-checker could theoretically allow some of these patterns, it's not likely to be worth the complexity to implement for the few cases it solves. (And even if it was thereotically worth the complexity, it's not likely that it would be priority over all the other improvements the TS team could be making, and that would be true even if they weren't busy rewriting a lot of the core tooling in another language)

amber trellis
wooden silo
#

Truth be told, I don't like that TS introduce type guards at all in this sense. I'm of the opinion, that if they introduce a feature they should either support all cases or none.

#

So they either shouldn't introduce it at all, or introduce it in such a way that works for all types.

magic stone
#

I think that's just not realistic for the problem of trying to put types on a very dynamic language like Javascript.

wooden silo
#

I just don't like half-delivered features. I don't find the same thing in Java or Haskell for example. Things either work for everything, or for nothign.

magic stone
#

It doesn't defeat the purpose, it's the only way to realistically have a language like Typescript

#

Java and Haskell were not trying to layer types on top of an existing, dynamic programming language.

wooden silo
#

If I were working with pure JS, I would write something like that:

function getField(obj, key) {
  if (key in obj) {
    return obj[key];
  }
  throw new Error();
}

And that would be enough for sound js.

#

I understand that it doesn't work like that in TS, but I don't see that as an explanation; i see it as a description of TS flaws.

magic stone
#

Being able to make pragmatic compromises about typesafety is the only reason Typescript exists and is popular.

wooden silo
#

I dislike that the only way for me to do something correct is to disable the checking.

magic stone
#

If you can't stop thinking in binary "it's completely correct, or it's totally wrong", you're never going to have any luck with this language.

wooden silo
#

I think these words "correct" and "wrong" are kinda absolute.

#

Doesn't make sense to say "almost correct".

#

It's like "completely vegetarian" or "almost vegetarian".

#

"almost vegetarian" is not-vegetarian.

#

"almost correct" is not-correct.

#

"almost sober" is not-sober.

#

"almost winner" is not-winner.

amber trellis
#

vegetarian is almost vegan

#

anyways i agree with retsam

magic stone
#

Java is an unsound language, then.

amber trellis
#

in ts terms, js is very far from correct, so ts has to deal with a lot of that

#

it's not going to deal with everything

magic stone
#

There's no such thing as a "mostly sound" language, and there exists soundness issues in Java, so it's an unsound language that nobody should use.

amber trellis
wooden silo
#

But js doesn't promise anyone anything. TypeScript promises to be transpiable and compatible with js.

amber trellis
#

and it is

wooden silo
#

So js stupidness is warranted. But typescript stupidness breaks the promise.

magic stone
#

Typescript does not promise to be a perfectly sound language. In fact, they explicitly call that out in places.

amber trellis
wooden silo
#

What do you guys want from me?

magic stone
#

I don't know what you want.

wooden silo
#

if I am to use something like typescript, I'd like to rely on its checks.

amber trellis
#

what do you want from ts?

subtle basin
#

Is there a mainstream language with a fully sound type system?

wooden silo
magic stone
#

You are not going to find anything to meet your standards.

wooden silo
amber trellis
wooden silo
#

It's statically-typed languages that can corner themselves, but some manage, like haskell.

amber trellis
wooden silo
amber trellis
subtle basin
#

Dynamically typed languages don't have a type system, unless you are talking about runtime type system then you might as well call all languages that.

wooden silo
#

There are:

  • statically typed languages
  • dynamically typed languages
  • languages that don't check types at all
#

Your confusion dynamic-typing with no-typing.

magic stone
#

Can we not get into an extended semantic debate?

amber trellis
wooden silo
#

It's not the same as not checking the types at all.

subtle basin
#

Yeah I'm just going to exit this conversation. If you are looking for a mainstream language with a fully sound type system, there isn't one.

amber trellis
amber trellis
wooden silo
#

Okay, different approach:

  • some language will check types and error in compilation (static typing)
  • some language will check types and error in runtime (dynamic typing)
  • some languages will not check types and not error (no typing)
magic stone
#

Yeah, I'm probably going to follow Burrito. If you want advice on how to best write Typescript, despite it not being perfectly sound feel free to ask. But I see nowhere productive from "Typescript does not meet my standard for correctness, therefore it's useless".

amber trellis
wooden silo
#

I was using python for like 10 years and I never stumbled upon any kind of contradiction in its type system.

#

But with typescript I stumble upon them very frequently.

amber trellis
#

(it barely has one)

wooden silo
#

And i'm doing the same things in both.

subtle basin
#

You can also express a lot less things using Python's type system.

#

If you constrain (read: cripple) TS's type system, then yes it can also be very sound.

amber trellis
wooden silo
#

where-as js will just return undefined or something else and not throw.

amber trellis
amber trellis
wooden silo
#

No, not true.

#

They're both dynamically typed, but python is strongly, dynamically typed, and js is weakly dynamically typed.

#

in python 1 + "2" will error.

#

You need to type str(1) + "2" or 1 + int("2").

#

The only correlation is that they aren't statically typed.

#

But other than that, they're very different.

amber trellis
#

right, js is more lax with coersion and bounds. however, these are separate things, js doesn't coerce stuff to undefined

wooden silo
#

In python if you try to read a missing prop or key, it will throw. js will simply return undefined without error.

amber trellis
#

love how much you're moving the goalposts here

#

i say one thing about how python doesn't check element types where java and c# do at runtime, and it explodes into this

#

i'm following retsam and burrito

wooden silo
#

And I say "it does check, just dynamically".

amber trellis
#

does list[int] error when you do .append("2")?

wooden silo
#

nope.

#

but neither does var: int = "2"

amber trellis
wooden silo
#

type-hints aren't checked at all.

amber trellis
#

good lord.

wooden silo
#

But this is not to say types aren't checked ๐Ÿ˜

#

Types are checked, type-hits aren't.

amber trellis
wooden silo
#

okay then.

amber trellis
#

you're very good at wildly misinterpreting messages

wooden silo
#

You responded with list[int] to my message about types, so I assumed you speak about checking types.