Open blakeembrey opened 7 years ago
@RyanCavanaugh Okay, so let me clarify: I intuitively read T extends U ? F<T> : G<T>
as T <: U ⊢ F(T), (T <: U ⊢ ⊥) ⊢ G(T)
, with the comparison done not piecewise, but as a complete step. That's distinctly different from "the union of for all {if t ∈ U then F({t}) else G({t}) | t ∈ T}
, which is what's currently the semantics.
(Pardon if my syntax is a bit off - my type theory knowledge is entirely self-taught, so I know I don't know all the syntactic formalisms.)
Which operation is more intuitive is up for infinite debate, but with the current rules it's easy to make a distributive type non-distributive with [T] extends [C]
. If the default were non-distributive, you'd need some new incantation at a different level to cause distributivity. That's also a separate question from which behavior is more often preferred; IME I almost never want a non-distributing type.
Ye there is no strong theoretical grounding for distribution because it’s a syntactic operation.
The reality is that it is very useful and trying to encode it some other way would be painful.
As it stands, I'll go ahead and trail off before I drive the conversation too far off topic.
there are so many issues about distrubutivness already, why won't we face it that new syntax is required?
Here is an example problem:
I want to specify that my users API endpoint/serice must NOT return any extra properties (like e.g. password) other than the ones specified in the service interface. If I accidentally return an object with extra properties, I want a compile time error, regardless of whether the result object has been produced by an object literal or otherwise.
A run time check of every returned object can be costly, especially for arrays.
Excess property checking doesn't help in this case. Honestly, I think its a wonky one-trick-pony solution. In theory it should've provided an "it just works" kind of experience - in practice its also a source of confusion Exact object types should have been implemented instead, they would have covered both use cases nicely.
@babakness Your type NoExcessiveProps
is a no-op. I think they mean something like this:
interface API {
username: () => { username: string }
}
const api: API = {
username: (): { username: string } => {
return { username: 'foobar', password: 'secret'} // error, ok
}
}
const api2: API = {
username: (): { username: string } => {
const id: <X>(x: X) => X = x => x;
const value = id({ username: 'foobar', password: 'secret' });
return value // no error, bad?
}
}
As the writer of the API type you want to enforce that username
just returns the username, but any implementer can get around that because object types have no width restriction. That can only be applied at the initialisation of a literal, which the implementer may, or may not, do. Though, I would heavily discourage anyone from trying to use exact types as language based security.
@spion
Excess property checking doesn't help in this case. Honestly, I think its a wonky one-trick-pony solution. In theory they should've provided an "it just works" kind of experience
EPC is a reasonably sensible and lightweight design choice that covers are large set of problem. The reality is that Exact types do not 'just work'. To implement in a sound way that supports extensibility requires a completely different type system.
@jack-williams Of course there would be other ways to verify present as well (runtime checks where performance is not an issue, tests etc) but an additional compile-time one is invaluable for fast feedback.
Also, I didn't mean that exact types "just work". I meant that EPC was meant to "just work" but in practice its just limited, confusing and unsafe. Mainly because if try to "deliberately" use it you generally end up shooting yourself in the foot.
edit: Yep, I edited to replace "they" with "it" as I realized its confusing.
@spion
Also, I didn't mean that exact types "just work". I meant that EPC was meant to "just work" but in practice its just limited, confusing and unsafe. Mainly because if try to "deliberately" use it you generally end up shooting yourself in the foot.
My mistake. Read the original comment as
In theory they should've provided an "it just works" kind of experience [which would have been exact types instead of EPC]
commentary in [] being my reading.
The revised statement:
In theory it should've provided an "it just works" kind of experience
is much clearer. Sorry for my misinterpretation!
type NoExcessiveProps<O> = {
[K in keyof O]: K extends keyof O ? O[K] : never
}
// no error
const getUser1 = (): {username: string} => {
const foo = {username: 'foo', password: 'bar' }
return foo
}
// Compile-time error, OK
const foo: NoExcessiveProps<{username: string}> = {username: 'a', password: 'b' }
// No error? 🤔
const getUser2 = (): NoExcessiveProps<{username: string}> => {
const foo = {username: 'foo', password: 'bar' }
return foo
}
The result for getUser2
is surprising, it feels inconsistent and like it should produce a compile-time error. Whats the insight on why it doesn't?
@babakness Your NoExcessiveProps
just evaluates back to T
(well a type with the same keys as T
). In [K in keyof O]: K extends keyof O ? O[K] : never
, K
will always be a key of O
since you are mapping over keyof O
. Your const
example errors because it triggers EPC just as it would have if you would have typed it as {username: string}
.
If you don't mind calling an extra function we can capture the actual type of the object passed in, and do a custom form of excess property checks. (I do realize the whole point is to automatically catch this type of error, so this might be of limited value):
function checked<T extends E, E>(o: T & Record<Exclude<keyof T, keyof E>, never>): E {
return o;
}
const getUser2 = (): { username: string } => {
const foo = { username: 'foo', password: 'bar' }
return checked(foo) //error
}
const getUser3 = (): { username: string } => {
const foo = { username: 'foo' }
return checked(foo) //ok
}
@dragomirtitian Ah... right... good point! So I'm trying to understand your checked
function. I'm particularly puzzled
const getUser2 = (): { username: string } => {
const foo = { username: 'foo', password: 'bar' }
const bar = checked(foo) // error
return checked(foo) //error
}
const getUser3 = (): { username: string } => {
const foo = { username: 'foo' }
const bar = checked(foo) // error!?
return checked(foo) //ok
}
The bar
assignment in getUser3
fails. The error seems to be at foo
Details of the error
The type for bar
here is {}
, which seems as though it is because on checked
function checked<T extends E, E>(o: T & Record<Exclude<keyof T, keyof E>, never>): E {
return o;
}
E
is not assigned anywhere. Yet if we replace typeof E
with typeof {}
, it doesn't work.
What is the type for E? Is there some kind of context-aware thing happening?
@babakness If there is no other place to infer a type parameter from, typescript will infer it from the return type. So when we are assigning the result of checked
to the return of getUser*
, E
will be the return type of the function, and T
will be the actual type of the value you want to return. If there is no place to infer E
from it will just default to {}
and so you will always get an error.
The reason I did it like this was to avoid any explicit type parameters, you could create a more explicit version of it:
function checked<E>() {
return function <T extends E>(o: T & Record<Exclude<keyof T, keyof E>, never>): E {
return o;
}
}
const getUser2 = (): { username: string } => {
const foo = { username: 'foo', password: 'bar' }
return checked<{ username: string }>()(foo) //error
}
const getUser3 = (): { username: string } => {
const foo = { username: 'foo' }
return checked<{ username: string }>()(foo) //ok
}
Note: The curried function approach is necessary since we don't yet have partial argument inference (https://github.com/Microsoft/TypeScript/pull/26349) so we can't specify some type parameter and have others inferred in the same call. To get around this we specify E
in the first call and let T
be inferred in the second call. You could also cache the cache
function for a specific type and use the cached version
function checked<E>() {
return function <T extends E>(o: T & Record<Exclude<keyof T, keyof E>, never>): E {
return o;
}
}
const checkUser = checked<{ username: string }>()
const getUser2 = (): { username: string } => {
const foo = { username: 'foo', password: 'bar' }
return checkUser(foo) //error
}
const getUser3 = (): { username: string } => {
const foo = { username: 'foo' }
return checkUser(foo) //ok
}
FWIW this is a WIP / sketch tslint rule that solves the specific problem of not accidentally returning extra properties from "exposed" methods.
https://gist.github.com/spion/b89d1d2958f3d3142b2fe64fea5e4c32
For the spread use case – see https://github.com/Microsoft/TypeScript/issues/12936#issuecomment-300382189 – could a linter detect a pattern like this and warn that it's not type-safe?
Copying code example from the aforementioned comment:
interface State {
name: string;
}
function nameReducer(state: State, action: Action<string>): State {
return {
...state,
fullName: action.payload // compiles, but it's an programming mistake
}
}
cc @JamesHenry / @armano2
Would very much like to see that happen. We use generated TypeScript definitions for GraphQL endpoints and it's a problem that TypeScript does not raise an error when I pass an object with more fields than necessary to a query because GraphQL will fail to execute such a query at runtime.
how much of this is now addressed with the 3.5.1 update w/ better checking for extra properties during assignment? we got a bunch of known problem areas flagged as errors the way we wanted them to be after upgrading to 3.5.1
if you have a problem and you think exact types are the right solution, please describe the original problem here
https://github.com/microsoft/TypeScript/issues/12936#issuecomment-284590083
Here's one involving React refs: https://github.com/microsoft/TypeScript/issues/31798
/cc @RyanCavanaugh
One use case for me is
export const mapValues =
<T extends Exact<T>, V>(object: T, mapper: (value: T[keyof T], key: keyof T) => V) => {
type TResult = Exact<{ [K in keyof T]: V }>;
const result: Partial<TResult> = { };
for (const [key, value] of Object.entries(object)) {
result[key] = mapper(value, key);
}
return result as TResult;
};
This is unsound if we don't use exact types, since if object
has extra properties, it's not safe to call mapper
on those extra keys and values.
The real motivation here is that I want to have the values for an enum somewhere that I can reuse in the code:
const choices = { choice0: true, choice1: true, choice2: true };
const callbacksForChoices = mapValues(choices, (_, choice) => () => this.props.callback(choice));
where this.props.callback
has type (keyof typeof choices) => void
.
So really it's about the type system being able to represent the fact that I have a list of keys in code land that exactly matches a set (e.g., a union) of keys in type land, so that we can write functions that operate on this list of keys and make valid type assertions about the result. We can't use an object (choices
in my previous example) because as far as the type system knows, the code-land object could have extra properties beyond whatever object type is used. We can't use an array (['choice0', 'choice1', 'choice2'] as const
, because as far as the type system knows, the array might not contain all of the keys allowed by the array type.
Maybe exact
shouldn't be a type, but only a modifier on function's inputs and/or output? Something like flow's variance modifier (+
/-
)
I want to add on to what @phaux just said. The real use I have for Exact
is to have the compiler guarantee the shape of functions. When I have a framework, I may want either of these: (T, S): AtMost<T>
, (T, S): AtLeast<T>
, or (T, S): Exact<T>
where the compiler can verify that the functions a user defines will fit exactly.
Some useful examples:
AtMost
is useful for config (so we don't ignore extra params/typos and fail early).
AtLeast
is great for things like react components and middleware where a user may shove whatever extra they want onto an object.
Exact
is useful for serialisation/deserialization (we can guarantee we don't drop data and these are isomorphic).
Would this help to prevent this from happening?
interface IDate {
year: number;
month: number;
day: number;
}
type TBasicField = string | number | boolean | IDate;
// how to make this generic stricter?
function doThingWithOnlyCorrectValues<T extends TBasicField>(basic: T): void {
// ... do things with basic field of only the exactly correct structures
}
const notADate = {
year: 2019,
month: 8,
day: 30,
name: "James",
};
doThingWithOnlyCorrectValues(notADate); // <- this should not work! I want stricter type checking
We really need a way in TS to say T extends exactly { something: boolean; } ? xxx : yyy
.
Or otherwise, something like:
const notExact = {
something: true,
name: "fred",
};
Will still return xxx
there.
Maybe const
keyword can be used? e.g.T extends const { something: boolean }
@pleerock it might be slightly ambiguous, as in JavaScript / TypeScript we can define a variable as const
but still add / remove object properties. I think the keyword exact
is pretty to the point.
I'm not sure if it's exactly related, but i'd expect at least two errors in this case: playground
@mityok I think that is related. I'm guessing you would like to do something along the lines of:
class Animal {
makeSound(): exact Foo {
return { a: 5 };
}
}
If the exact
made the type stricter - then it shouldn't be extendable with an extra property, as you've done in Dog
.
taking advantage of the const
(as const
) and using before interfaces and types, like
const type WillAcceptThisOnly = number
function f(accept: WillAcceptThisOnly) {
}
f(1) // error
f(1 as WillAcceptThisOnly) // ok, explicit typecast
const n: WillAcceptThisOnly = 1
f(n) // ok
would be really verbose having to assign to const variables, but would avoid a lot of edge cases when you pass a typealias that wasn't exact what you were expecting
I have came up with pure TypeScript solution for Exact<T>
problem that, I believe, behaves exactly like what has been requested in the main post:
// (these two types MUST NOT be merged into a single declaration)
type ExactInner<T> = <D>() => (D extends T ? D : D);
type Exact<T> = ExactInner<T> & T;
function exact<T>(obj: Exact<T> | T): Exact<T> {
return obj as Exact<T>;
};
The reason ExactInner
must be not included in the Exact
is due to #32824 fix not being released yet (but already merged in !32924).
It's only possible to assign a value to the variable or function argument of type Exact<T>
, if the right hand expression is also Exact<T>
, where T
is exactly identical type in both parts of assignment.
I haven't achieved automatic promotion of values into Exact types, so that's what exact()
helper function is for. Any value can be promoted to be of exact type, but assignment will only succeed if TypeScript can prove that underlying types of both parts of expression are not just extensible, but exactly the same.
It works by exploiting the fact that TypeScript uses extend
relation check to determine if right hand type can be assigned to the left hand type — it only can if right hand type (source) extends the left hand type (destination).
Quoting checker.ts
,
// Two conditional types 'T1 extends U1 ? X1 : Y1' and 'T2 extends U2 ? X2 : Y2' are related if // one of T1 and T2 is related to the other, U1 and U2 are identical types, X1 is related to X2, // and Y1 is related to Y2.
ExactInner<T>
generic uses the described approach, substituting U1
and U2
with underlying types that require exactness checks. Exact<T>
adds an intersection with plain underlying type, which allows TypeScript to relax exact type when its target variable or function argument are not an exact type.
From programmer's perspective, Exact<T>
behaves as if it sets an exact
flag on T
, without inspecting T
or changing it, and without creating an independent type.
Here are playground link and gist link.
Possible future improvement would be to allow auto-promotion of non-exact types into exact types, completely removing the need in exact()
function.
Amazing work @toriningen!
If anyone is able to find a way to make this work without having to wrap your value in a call to exact
it would be perfect.
Not sure if this is the right issue, but here is an example of something I'd like to work.
enum SortDirection {
Asc = 'asc',
Desc = 'desc'
}
function addOrderBy(direction: "ASC" | "DESC") {}
addOrderBy(SortDirection.Asc.toUpperCase());
@lookfirst That's different. This is asking for a feature for types that don't admit extra properties, like some type exact {foo: number}
where {foo: 1, bar: 2}
isn't assignable to it. That's just asking for text transforms to apply to enum values, which likely doesn't exist.
Not sure if this is the right issue, but [...]
In my experience as a maintainer elsewhere, if you're in doubt and couldn't find any clear existing issue, file a new bug and worst case scenario, it gets closed as a dupe you didn't find. This is pretty much the case in most major open source JS projects. (Most of us bigger maintainers in the JS community are actually decent people, just people who can get really bogged down over bug reports and such and so it's hard not to be really terse at times.)
@isiahmeadows Thanks for the response. I didn't file a new issue because I was searching for duplicate issues first, which is the correct thing to do. I was trying to avoid bogging people down because I wasn't sure if this was the right issue or not or even how to categorize what I was talking about.
EDITED: Check @aigoncharov solution bellow, because I think is even faster.
type Exact<T, R> = T extends R
? R extends T
? T
: never
: never
Don't know if this can be improved more.
type Exact<T, Shape> =
// Check if `T` is matching `Shape`
T extends Shape
// Does match
// Check if `T` has same keys as `Shape`
? Exclude<keyof T, keyof Shape> extends never
// `T` has same keys as `Shape`
? T
// `T` has more keys than `Shape`
: never
// Does not match at all
: never;
type InexactType = {
foo: string
}
const obj = {
foo: 'foo',
bar: 'bar'
}
function test1<T>(t: Exact<T, InexactType>) {}
function test2(t: InexactType) {}
test1(obj) // $ExpectError
test2(obj)
Without comments
type ExactKeys<T1, T2> = Exclude<keyof T1, keyof T2> extends never
? T1
: never
type Exact<T, Shape> = T extends Shape
? ExactKeys<T, Shape>
: never;
Don't know if this can be improved more.
type Exact<T, Shape> = // Check if `T` is matching `Shape` T extends Shape // Does match // Check if `T` has same keys as `Shape` ? Exclude<keyof T, keyof Shape> extends never // `T` has same keys as `Shape` ? T // `T` has more keys than `Shape` : never // Does not match at all : never; type InexactType = { foo: string } const obj = { foo: 'foo', bar: 'bar' } function test1<T>(t: Exact<T, InexactType>) {} function test2(t: InexactType) {} test1(obj) // $ExpectError test2(obj)
Without comments
type ExactKeys<T1, T2> = Exclude<keyof T1, keyof T2> extends never ? T1 : never type Exact<T, Shape> = T extends Shape ? ExactKeys<T, Shape> : never;
Love that idea!
Another trick that could do the job is to check assignability in both directions.
type Exact<T, R> = T extends R
? R extends T
? T
: never
: never
type A = {
prop1: string
}
type B = {
prop1: string
prop2: string
}
type C = {
prop1: string
}
type ShouldBeNever = Exact<A, B>
type ShouldBeA = Exact<A, C>
Another playground from @iamandrewluca https://www.typescriptlang.org/play/?ssl=7&ssc=6&pln=7&pc=17#code/C4TwDgpgBAogHgQwMbADwBUA0UBKA+KAXinSgjmAgDsATAZ1wCgooB+XMi6+k5lt3vygAuKFQgA3CACc+o8VNmNQkKAElxiFOnDRiAbz4sAZgHtTousGkBLKgHNGAX0aMkpqlaimARgCsiKEMhMwsoAHJQ8MwjKB8EaVFw+Olw51djAFcqFBsPKEorAEYMPAAKYFF4ZDQsdU0anUg8AEoglyyc4DyqAogrACYK0Q1yRt02-RdlfuAist8-NoB6ZagAEnhIFBhpaVNZQuAhxZagA
A nuance here is whether Exact<{ prop1: 'a' }>
should be assignable to Exact<{ prop1: string }>
. In my use cases, it should.
@jeremybparagon your case is covered. Here are some more cases.
type InexactType = {
foo: 'foo'
}
const obj = {
// here foo is infered as `string`
// and will error because `string` is not assignable to `"foo"`
foo: 'foo',
bar: 'bar'
}
function test1<T>(t: Exact<T, InexactType>) {}
function test2(t: InexactType) {}
test1(obj) // $ExpectError
test2(obj) // $ExpectError
type InexactType = {
foo: 'foo'
}
const obj = {
// here we cast to `"foo"` type
// and will not error
foo: 'foo' as 'foo',
bar: 'bar'
}
function test1<T>(t: Exact<T, InexactType>) {}
function test2(t: InexactType) {}
test1(obj) // $ExpectError
test2(obj)
type Exact<T, R> = T extends R
? R extends T
? T
: never
: never
I think anybody using this trick (and I'm not saying there aren't valid uses for it) should be acutely aware that it is very very easy to get more props in the "exact" type. Since InexactType
is assignable to Exact<T, InexactType>
if you have something like this, you break out of exactness without realizing it:
function test1<T>(t: Exact<T, InexactType>) {}
function test2(t: InexactType) {
test1(t); // inexactType assigned to exact type
}
test2(obj) // but
This is the reason (at least one of them) that TS does not have exact types, as it would require a complete forking of object types in exact vs non-exact types where an inexact type is never assignable to an exact one, even if at face value they are compatible. The inexact type may always contain more properties. (At least this was one of the reasons @ahejlsberg mentioned as tsconf).
If asExact
were some syntactic way of marking such an exact object, this is what such a solution might look like:
declare const exactMarker: unique symbol
type IsExact = { [exactMarker]: undefined }
type Exact<T extends IsExact & R, R> =
Exclude<keyof T, typeof exactMarker> extends keyof R? T : never;
type InexactType = {
foo: string
}
function asExact<T>(o: T): T & IsExact {
return o as T & IsExact;
}
const obj = asExact({
foo: 'foo',
});
function test1<T extends IsExact & InexactType>(t: Exact<T, InexactType>) {
}
function test2(t: InexactType) {
test1(t); // error now
}
test2(obj)
test1(obj); // ok
const obj2 = asExact({
foo: 'foo',
bar: ""
});
test1(obj2);
const objOpt = asExact < { foo: string, bar?: string }>({
foo: 'foo',
bar: ""
});
test1(objOpt);
@dragomirtitian that's why I came up with the solution a bit earlier https://github.com/microsoft/TypeScript/issues/12936#issuecomment-524631270 that doesn't suffer from this.
@dragomirtitian it's a matter of how you type your functions. If you do it a little differently, it works.
type Exact<T, R> = T extends R
? R extends T
? T
: never
: never
type InexactType = {
foo: string
}
const obj = {
foo: 'foo',
bar: 'bar'
}
function test1<T>(t: Exact<T, InexactType>) {}
function test2<T extends InexactType>(t: T) {
test1(t); // fails
}
test2(obj)
@jeremybparagon your case is covered.
@iamandrewluca I think the solutions here and here differ on how they treat my example.
type Exact<T, R> = T extends R
? R extends T
? T
: never
: never
type A = {
prop1: 'a'
}
type C = {
prop1: string
}
type ShouldBeA = Exact<A, C> // This evaluates to never.
const ob...
@aigoncharov The problem is you need to be aware of that so one could easily not do this and test1
could still get called with extra properties. IMO any solution that can so easily allow an accidental inexact assignment has already failed as the whole point is to enforce exactness in the type system.
@toriningen yeah your solution seems better, I was just referring to the last posted solution. Your solution has going for it the fact that you don't need the extra function type parameter, however it does not seem to work well for optional properties:
// (these two types MUST NOT be merged into a single declaration)
type ExactInner<T> = <D>() => (D extends T ? D : D);
type Exact<T> = ExactInner<T> & T;
type Unexact<T> = T extends Exact<infer R> ? R : T;
function exact<T>(obj: Exact<T> | T): Exact<T> {
return obj as Exact<T>;
};
////////////////////////////////
// Fixtures:
type Wide = { foo: string, bar?: string };
type Narrow = { foo: string };
type ExactWide = Exact<Wide>;
type ExactNarrow = Exact<Narrow>;
const ew: ExactWide = exact<Wide>({ foo: "", bar: ""});
const assign_en_ew: ExactNarrow = ew; // Ok ?
@jeremybparagon I'm not sure @aigoncharov 's solution does a good job on optional properties though. Any solution based on T extends S
and S extends T
will suffer from the simple fact that
type A = { prop1: string }
type C = { prop1: string, prop2?: string }
type CextendsA = C extends A ? "Y" : "N" // Y
type AextendsC = A extends C ? "Y" : "N" // also Y
I think @iamandrewluca of using Exclude<keyof T, keyof Shape> extends never
is good, my type is quite similar (I edited my original answer to add the &R
to ensure T extends R
without any extra checks).
type Exact<T extends IsExact & R, R> =
Exclude<keyof T, typeof exactMarker> extends keyof R? T : never;
I would not stake my reputation that my solution does not have holes though, I haven't looked that hard for them but welcome any such findings 😊
we should have a flag where this is enabled globally. In this way, who wants to loose type can keep doing the same. Way too many bugs caused by this issue. Now I try to try to avoid spread operator and use pickKeysFromObject(shipDataRequest, ['a', 'b','c'])
Here's a use case for exact types I recently stumbled on:
type PossibleKeys = 'x' | 'y' | 'z';
type ImmutableMap = Readonly<{ [K in PossibleKeys]?: string }>;
const getFriendlyNameForKey = (key: PossibleKeys) => {
switch (key) {
case 'x':
return 'Ecks';
case 'y':
return 'Why';
case 'z':
return 'Zee';
}
};
const myMap: ImmutableMap = { x: 'foo', y: 'bar' };
const renderMap = (map: ImmutableMap) =>
Object.keys(map).map(key => {
// Argument of type 'string' is not assignable to parameter of type 'PossibleKeys'
const friendlyName = getFriendlyNameForKey(key);
// No index signature with a parameter of type 'string' was found on type 'Readonly<{ x?: string | undefined; y?: string | undefined; z?: string | undefined; }>'.
return [friendlyName, map[key]];
});
;
Because types are inexact by default, Object.keys
has to return a string[]
(see https://github.com/microsoft/TypeScript/pull/12253#issuecomment-263132208), but in this case, if ImmutableMap
was exact, there's no reason it couldn't return PossibleKeys[]
.
@dallonf note that this example requires extra functionality besides just exact types -- Object.keys
is just a function and there'd need to be some mechanism for describing a function that returns keyof T
for exact types and string
for other types. Simply having the option to declare an exact type wouldn't be sufficient.
@RyanCavanaugh I think that was the implication, exact types + the ability to detect them.
Use case for the react typings:
forwardRef<T, P>(render: (props: P, ref: Ref<T>) => ReactElement<P> & { displayName?: string }) => ComponentType<P>
.
It's tempting to pass a regular component to forwardRef
which is why React issues runtime warnings if it detects propTypes
or defaultProps
on the render
argument. We'd like to express this at the type level but have to fallback to never
:
- forwardRef<T, P>(render: (props: P, ref: Ref<T>) => ReactElement<P> & { displayName?: string }) => ComponentType<P>
+ forwardRef<T, P>(render: (props: P, ref: Ref<T>) => ReactElement<P> & { displayName?: string, propTypes?: never, defaultProps?: never }) => ComponentType<P>
The error message with never
is not helpful ("{} is not assignable to undefined").
Can someone help me out on how @toriningen's solution would look like with a union of different event object shapes? I want to restrict my event shapes in redux-dispatch calls, e.g.:
type StoreEvent =
| { type: 'STORE_LOADING' }
| { type: 'STORE_LOADED'; data: unknown[] }
It's unclear how I could make a typed dispatch() function which only accepts the exact shape of an event.
(UPDATE: I figured it out: https://gist.github.com/sarimarton/d5d539f8029c01ca1c357aba27139010)
Use case:
Missing Exact<>
support leads to runtime problems with GraphQL mutations. GraphQL accepts exact list of permitted properties. If you provide excessive props, it throws an error.
So when we obtain some data from the form, then Typescript cannot validate excess (extra) properties. And we will get an error at runtime.
The following example illustrates imaginary safety
According to the article https://fettblog.eu/typescript-match-the-exact-object-shape/ and similar solutions provided above we can use the following ugly solution:
savePerson<T>(person: ValidateShape<T, Person>)
solution is Ugly?Assume you have deeply nested input type eg.:
// Assume we are in the ideal world where implemented Exact<>
type Person {
name: string;
address: Exact<Address>;
}
type Address {
city: string
location: Exact<Location>
}
type Location {
lon: number;
lat: number;
}
savePerson(person: Exact<Person>)
I cannot imagine what spaghetti we should write to get the same behavior with the currently available solution:
savePerson<T, TT, TTT>(person:
ValidateShape<T, Person keyof ...🤯...
ValidateShape<TT, Address keyof ...💩...
ValidateShape<TTT, Location keyof ...🤬...
> > >)
So, for now, we have big holes in static analysis in our code, which works with complex nested input data.
This is a proposal to enable a syntax for exact types. A similar feature can be seen in Flow (https://flowtype.org/docs/objects.html#exact-object-types), but I would like to propose it as a feature used for type literals and not interfaces. The specific syntax I'd propose using is the pipe (which almost mirrors the Flow implementation, but it should surround the type statement), as it's familiar as the mathematical absolute syntax.
This syntax change would be a new feature and affect new definition files being written if used as a parameter or exposed type. This syntax could be combined with other more complex types.
Apologies in advance if this is a duplicate, I could not seem to find the right keywords to find any duplicates of this feature.
Edit: This post was updated to use the preferred syntax proposal mentioned at https://github.com/Microsoft/TypeScript/issues/12936#issuecomment-267272371, which encompasses using a simpler syntax with a generic type to enable usage in expressions.