Skip to content

Nominal typing and TypeScript > 2.7 #21625

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

Closed
alexandru opened this issue Feb 5, 2018 · 13 comments
Closed

Nominal typing and TypeScript > 2.7 #21625

alexandru opened this issue Feb 5, 2018 · 13 comments
Labels
Discussion Issues which may not have code impact

Comments

@alexandru
Copy link

Hello,

TypeScript is structurally typed, but sometimes we need nominal typing, for interfaces / types, not classes.

For example one instance where nominal typing is needed is in encoding higher kinded types, which are needed in functional programming libraries, like fp-ts and funfix. The encoding currently used goes like this:

interface HK<F, A> {
  readonly _URI: F
  readonly _A: A
}

These are known as "phantom types", aka types that don't have a runtime footprint, their sole purpose being to aid the compiler in doing static type checks.

Unfortunately TypeScript 2.7 is breaking this encoding due to its new restriction to have all readonly properties initialized in the constructor. So if we are to keep using such an encoding, then we'd have to do:

class Box<A> extends HK<"box", A> {
  readonly _URI: "box" = undefined as any
  readonly _A: A = undefined as any
}

But this is not acceptable, because it has runtime ramifications, both _URI and _A being registered as properties on Box.prototype and that's very uncool, for one because it implies extra memory usage, but also because this encoding is very TypeScript-specific, as for JavaScript engines / code this has no value and the optimal encoding for Flow is different.

The current proposal is to use literal properties (with special chars that would make them unsuitable for usage as normal properties), which apparently TypeScript 2.7 doesn't touch:

interface HK<F, A> {
  readonly '-URI': F
  readonly '-A': A
}

My worry however is that this will be a temporary workaround.

I really do appreciate the tightening of the type system and the new strict settings that keep getting added, however as library authors we need an encoding that doesn't break between versions with the strictest settings active.

So, can we get an assurance that the above encoding won't break, or that if it does, it will get replaced by a feature that fixes this need (encoding of phantom types) for good?

And thanks for all your hard work 👏, in spite of TypeScript 2.7 breaking my code, the improvements look good and I understand more is on the way for 2.8.

Cheers,

@jack-williams
Copy link
Collaborator

Does the definite assignment assertion operator (!) address this?

class Box<A> implements HK<"box", A> {
    readonly _URI!: "box";
    readonly _A!: A;
}

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Feb 5, 2018

Alternatively, declaration merging can resolve this, although @jack-williams's suggestion might be desirably more explicit.

interface HK<F, A> {
    readonly _URI: F
    readonly _A: A
}

interface Box<A> extends HK<"box", A> { }
class Box<A> {}

@RyanCavanaugh RyanCavanaugh added the Discussion Issues which may not have code impact label Feb 5, 2018
@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Feb 5, 2018

So, can we get an assurance that the above encoding won't break, or that if it does, it will get replaced by a feature that fixes this need (encoding of phantom types) for good?

@ahejlsberg seems like a reasonable commitment?

@zpdDG4gta8XKpMCd
Copy link

is there a chance we can see true hkt? i promise they are better than instantiating abstract classes

@alexandru
Copy link
Author

@jack-williams

Does the definite assignment assertion operator (!) address this?

I think yes, if it continues to work. The transition is a little painful as it's not compatible with TS 2.6, but I guess everybody will eventually upgrade.

@KiaraGrouwstra
Copy link
Contributor

As it hadn't been mentioned yet, newtype-ts (also by @gcanti) extracts this pattern from fp-ts for reuse.

By the way, the TS compiler uses phantom type constructs for nominal typing internally itself as well, while a regression test is also present already. Maybe adding an extra test for this HK/Box scenario should do it?

@weswigham
Copy link
Member

@alexandru the link you posted said the optimal representation in flow is a type level function; now that we have conditional types and ReturnType<T>, the same may be true of TS.

@KiaraGrouwstra
Copy link
Contributor

The function types they're passing to HKT from typeclass instances (e.g. array) appear to be generic function types, i.e. have return types dependent on the input types.
I wonder if that'd form a barrier here for TS's ReturnType, which doesn't cover input-dependent return types yet.

@goodmind
Copy link

any updates?

@fenduru
Copy link

fenduru commented May 31, 2018

2.7 also introduced unique symbol, which you can use to create an unaccessable property without it being readonly. I use this in a dependency management system that uses strings as dependency names, but wants to have associated types for when they're used. Example:

declare const phantom: unique symbol;
type Dep<T> = string & { [phantom]?: T };

const foo: Dep<number> = 'foo';

function inject<T>(deps: [Dep<T>], callback: (a: T) => void) { }

inject([foo], (a) => {
    console.log(a) // a is a number
})

playground link

@zhirzh
Copy link

zhirzh commented Jul 23, 2018

on a side note, what is the meaning behind URI property?
is there some deep rooted reason behind it or can it be anything else - like type or kind?

@chancecarey
Copy link

Bumping this up - still an issue that needs to be looked at. Nominal typing is required imo.

@orta
Copy link
Contributor

orta commented Oct 22, 2019

You probably want to follow #202 for that

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Discussion Issues which may not have code impact
Projects
None yet
Development

No branches or pull requests