1

I learnt that the

rule for TypeScript’s structural type system is that x is compatible with y if y has at least the same members as x

This allows an assignment of a variable of a subtype to a variable of a supertype. Is there a way to get a compile-time error regarding that assignment?

(TypeScript Playground)

interface SuperT {
    field: string
}

// an explicitly declared subtype object for a supertype variable generates an error
const super1: SuperT = {field: 'value', extra: 1} // compile-time error: Type '{ field: string; extra: number; }' is not assignable to type 'SuperT'

function subTValue() { return {field: 'value', extra: 1} }
const super2: SuperT = subTValue() // no compile-time error, BUT HOW TO get a compile-time error here? 
awho
  • 431
  • 5
  • 13
  • Interesting find. I added a return type to the definition of `subTValue`, turned on all the checks, and still no error. Need to look into this! Probably there is something in the docs that define the behavior perfectly, but it is, I have to say, pretty interesting.... – Ray Toal May 29 '19 at 00:30
  • @jcalz, trying to find a solution, I saw you answered many typescript-related questions, even some close to mine, but not exactly the same. I hope you are reading this... :) – awho May 29 '19 at 00:32
  • 1
    Looks similar to [this question](https://stackoverflow.com/q/54775790/831878). Is that it? – Ray Toal May 29 '19 at 01:00
  • 1
    @awho I'm looking – jcalz May 29 '19 at 01:41

2 Answers2

2

You want exact types which aren't directly supported. You can do various tricks with generics and conditional types to get closer. Here's one way to do it indirectly:

interface SuperT {
    field: string
}

type Exactly<T, U extends T> = T & Record<Exclude<keyof U, keyof T>, never>;
const asExactlySuperT = <U extends Exactly<SuperT, U>>(superT: U) => superT;

const superOkay: SuperT = asExactlySuperT({ field: "a" }); // okay

function subTValue() { return { field: 'value', extra: 1 } }
const superBad: SuperT = asExactlySuperT(subTValue()); // error! 
// types of property "extra" are incompatible

Link to code

The idea there is that Exactly<T, U> will take a type T and a candidate type U which hopefully matches T exactly with no extra properties. If it does, then Exactly<T, U> will equal U. If it does not, then Exactly<T, U> will set the property types of any extra properties to never. Since asExactlySuperT<U>() requires that U extends Exactly<SuperT, U>, the only way that can happen is if there are no extra properties in U.

Hope that helps. Good luck!

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • If for some reason the object you want to check has type `any`, no error will be raised (`const superBad: SuperT = asExactlySuperT(subTValue() as any); // no error`). Wondering if `asExactlySuperT` could deal with `any` objects, too, or if it can raise an error like "don't send me a param of type `any` as I can't deal with it". – awho Jun 28 '19 at 10:29
  • `any` is a deliberate escape hatch from the type system. It's seen as both a supertype and a subtype of every other type (that's unsound but useful) so it will tend to allow things no matter what. That's the point of it. It is [possible to detect `any`](https://stackoverflow.com/a/49928360/2887218) and do something different with `any` than the default behavior, but be careful doing that... the expectation of `any` is that it will "just make things work", and explicitly failing on `any` might surprise people. – jcalz Jun 28 '19 at 13:52
0

As Ray Toal found out, an answer for a very similar issue can be found here. (And I knew this in the first place, I just wanted to check jcalz's reaction time. Pretty impressive, @jcalz!)

Based on that approach, my code would look like:

(TypeScript Playground)

type StrictPropertyCheck<T, TExpected, TError> = Exclude<keyof T, keyof TExpected> extends never ? {} : TError

interface SuperT {
    field: string
}

function doIdentity<T extends SuperT>(a: T & StrictPropertyCheck<T, SuperT, "Only allowed properties of SuperT">) {
    return a
}


function subTValue() { return { field: 'value', extra: 1 } }

const super3: SuperT = doIdentity(subTValue()) // we do get a compile-time error!
awho
  • 431
  • 5
  • 13
  • 1
    @jcalz, just to be clear, it was a joke, I was sincerely looking for a solution, I wasn't aware of that answer when I posted my q :) Thanks – awho May 29 '19 at 02:02